lib/metro/events/event_relay.rb
require_relative 'event_data'
module Metro
#
# An EventRelay represents a target's willingness to respond to events
# generate from the window. An event relay is generate for every scene
# but additional relays can be generated to also listen for events.
#
# An event relay can register a target to listen for the following window
# events: 'button_down'; 'button_up'; and 'button_held'.
#
# @note registering for 'button_held' events require that the window be
# speicfied. As that is the only way to ask if the button is currently
# pressed.
#
# @see #on_up
# @see #on_down
# @see #on_hold
#
class EventRelay
#
# Defines the provided controls for every EventRelay that is created.
#
# @see #define_control
#
# @param [Array<ControlDefinition>] controls the definitions of controls
# that should be added to all EventRelays.
#
def self.define_controls(controls)
controls.each { |control| define_control control }
end
#
# Defines a control from a ControlDefinition for all EventRelays. A
# control is a way of defining a shortcut for a common event. This
# could be the use of a common set of keys for confirmation or canceling.
#
def self.define_control(control)
check_for_already_defined_control!(control)
define_method control.name do |&block|
send(control.event,*control.args,&block)
end
end
def self.check_for_already_defined_control!(control)
if instance_methods.include? control.name
error! "error.reserved_control_name", name: control.name
end
end
#
# An event relay is created a with a target and optionally a window.
#
# @param [Object] target the object that will execute the code when
# the button events have fired have been triggered.
# @param [Window] window the window of the game. This parameter is
# optional and only required if the events are interested in buttons
# being held.
#
def initialize(target,window = nil)
@target = target
@window = window
@up_actions ||= {}
@down_actions ||= {}
@held_actions ||= {}
@mouse_movement_actions ||= []
@custom_notifications ||= HashWithIndifferentAccess.new([])
end
attr_reader :target, :window
#
# Register for a button_down event. These events are fired when
# the button is pressed down. This event only fires once when the
# button moves from the not pressed to the down state.
#
# @example Registering for a button down event to call a method named 'previous_option'
#
# class ExampleScene
# event :on_down, GpLeft, GpUp, do: :previous_option
#
# def previous_option
# @selected_index = @selected_index - 1
# @selected_index = options.length - 1 if @selected_index <= -1
# end
# end
#
# Here in this scene if the GpLeft or GpUp buttons are pressed down the method
# `previous_options` will be executed.
#
#
# @example Registering for a button down event with a block of code to execute
#
# class ExampleScene
# event :on_down, GpLeft, GpUp do
# @selected_index = @selected_index - 1
# @selected_index = options.length - 1 if @selected_index <= -1
# end
# end
#
# This example uses a block instead of a method name but it is absolultey the same
# as the last example.
#
def on_down(*args,&block)
_on(@down_actions,args,block)
end
alias_method :button_down, :on_down
#
# Register for a button_up event. These events are fired when
# the button is released (from being pressed down). This event only fires
# once when the button moves from the pressed state to the up state.
#
# @example Registering for a button down event to call a method named 'next_option'
#
# class ExampleScene
# event :on_up, KbEscape, do: :leave_scene
#
# def leave_scene
# transition_to :title
# end
# end
#
# Here in this scene if the Escape Key is pressed and released the example scene
# will transition to the title scene.
#
# @example Registering for a button up event with a block of code to execute
#
# class ExampleScene
# event :on_up, KbEscape do
# transition_to :title
# end
# end
#
# This example uses a block instead of a method name but it is absolultey the same
# as the last example.
#
def on_up(*args,&block)
_on(@up_actions,args,block)
end
alias_method :button_up, :on_up
#
# Register for a button_held event. These events are fired when
# the button is currently in the downstate. This event continues to fire at the
# beginning of every update of a scene until the button is released.
#
# @note button_held events require that the window be specified during initialization.
#
# @example Registering for button held events
#
# class ExampleScene
# event :on_hold KbLeft, GpLeft do
# player.turn_left
# end
#
# event :on_hold, KbRight, GpRight do
# player.turn_right
# end
#
# event :on_hold, KbUp, Gosu::GpButton0, do: :calculate_accleration
#
# def calculate_acceleration
# long_complicated_calculated_result = 0
# # ... multi-line calculations to determine the player acceleration ...
# player.accelerate = long_complicated_calculated_result
# end
# end
#
def on_hold(*args,&block)
log.warn "Registering for a on_hold event requires that a window be provided." unless window
_on(@held_actions,args,block)
end
alias_method :button_hold, :on_hold
alias_method :button_held, :on_hold
#
# Register for mouse movements events. These events are fired each update
# providing an event which contains the current position of the mouse.
#
# @note mouse movement events fire with each update so it is up to the
# receiving object of the event to determine if the new mouse movement
# is a delta.
#
# @note mouse movement events require that the window be specified during initialization.
#
# @example Registering for button held events
#
# class ExampleScene
#
# draws :player
#
# event :on_mouse_movement do |event|
# player.position = event.mouse_point
# end
# end
#
def on_mouse_movement(*args,&block)
options = (args.last.is_a?(Hash) ? args.pop : {})
@mouse_movement_actions << ( block || lambda { |instance| send(options[:do]) } )
end
#
# Register for a custom notification event. These events are fired when
# another object within the game posts a notification with matching criteria.
# If there has indeed been a match, then the stored action block will be fired.
#
# When the action block is specified is defined with no parameters it is assumed that
# that the code should be executed within the context of the object that defined
# the action, the 'target'.
#
# @example Registering for a save complete event that would re-enable a menu.
#
# class ExampleScene
# event :notification, :save_complete do
# menu.enabled!
# end
# end
#
# The action block can also be specified with two parameters. In this case the code is
# no longer executed within the context of the object and is instead provided the
# the action target and the action source.
#
# @example Registering for a win game event that explicitly states the target and source.
#
# class ExampleScene
#
# event :notification, :win_game do |target,winner|
# target.declare_winner winner
# end
#
# def declare_winner(winning_player)
# # ...
# end
# end
#
def notification(param,&block)
custom_notifications[param.to_sym] = custom_notifications[param.to_sym] + [ block ]
end
attr_reader :up_actions, :down_actions, :held_actions
attr_reader :mouse_movement_actions
attr_reader :custom_notifications
def _on(hash,args,block)
options = (args.last.is_a?(Hash) ? args.pop : {})
args.each do |keystroke|
hash[keystroke] = block || lambda { |instance| send(options[:do]) }
end
end
#
# This is called by external or parent source of events, usually a Scene, when a button up event
# has been triggered.
#
def fire_button_up(id)
execute_block_for_target( &up_action(id) )
end
#
# This is called by external or parent source of events, usually a Scene, when a button down
# event has been triggered.
#
def fire_button_down(id)
execute_block_for_target( &down_action(id) )
end
#
# Fire the events mapped to the held buttons within the context
# of the specified target. This method is differently formatted because held buttons are not
# events but polling to see if the button is still being held.
#
def fire_events_for_held_buttons
held_actions.each do |key,action|
execute_block_for_target(&action) if window and window.button_down?(key)
end
end
#
# Fire events for all the registered actions that are suppose to receive
# the mouse movement events.
#
def fire_events_for_mouse_movement
mouse_movement_actions.each do |action|
execute_block_for_target(&action)
end
end
def execute_block_for_target(&block)
event_data = EventData.new(window)
target.instance_exec(event_data,&block)
end
# @return a block of code that is mapped for the 'button_up' id
def up_action(id)
up_actions[id] || lambda {|no_op| }
end
# @return a block of code that is mapped for the 'button_down' id
def down_action(id)
down_actions[id] || lambda {|no_op| }
end
#
# Fire all events mapped to the matching notification.
#
def fire_events_for_notification(event,sender)
notification_actions = custom_notifications[event]
notification_actions.each do |action|
_fire_event_for_notification(event,sender,action)
end
end
#
# Fire a single event based on the matched notification.
#
# An action without any parameters is assumed to be executed within the contexxt
# of the target. If there are two parameters we will simply execute the action and
# pass it both the target and the sender.
#
def _fire_event_for_notification(event,sender,action)
if action.arity == 2
target.instance_exec(sender,event,&action)
elsif action.arity == 1
target.instance_exec(sender,&action)
else
target.instance_eval(&action)
end
end
end
end