burtlo/metro

View on GitHub
lib/metro/scene.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require_relative 'views/scene_view'
require_relative 'events/events'
require_relative 'models/draws'

require_relative 'animation/has_animations'
require_relative 'animation/scene_animation'
require_relative 'animation/after_interval_factory'

module Metro

  #
  # A scene is a basic unit of a game. Within a scene you define a number of methods
  # that handle the initial setup, event configuration, logic updating, and drawing.
  #
  # @see #show
  # @see #update
  # @see #draw
  #
  # A fair number of private methods within Scene are prefaced with an underscore.
  # These methods often call non-underscored methods within those methods. This allows
  # for scene to configure or perform some functionality, while providing an interface
  # so that every subclass does not have to constantly call `super`.
  #
  class Scene
    include Units

    #
    # As Scene does a lot of work for you with regarding to setting up content, it is
    # best not to override #initialize and instead define an #after_initialize method
    # within the subclasses of Scene.
    #
    # @note This method should be implemented in the Scene subclass.
    #
    def after_initialize ; end

    #
    # This method is called right after the scene has been adopted by the window
    #
    # @note This method should be implemented in the Scene subclass.
    #
    def show ; end

    #
    # This is called every update interval while the window is being shown.
    #
    # @note This method should be implemented in the Scene subclass.
    #
    def update ; end

    #
    # This is called after every {#update} and when the OS wants the window to
    # repaint itself.
    #
    # @note This method should be implemented in the Scene subclass.
    #
    def draw ; end

    #
    # Before a scene is transitioned away from to a new scene, this method is called
    # to allow for the scene to complete any tasks, stop any actions, or pass any
    # information from the existing scene to the scene that is about to replace it.
    #
    # @note This method should be implemented in the Scene subclass.
    #
    # @param [Scene] new_scene this is the instance of the scene that is about to replace
    #   the current scene.
    #
    def prepare_transition_to(new_scene) ; end

    #
    # Before a scene is transitioned to it is called with the previous scene. This
    # allows for the new scene to retrieve any data from the previous scene to assist
    # with the layout of the current scene.
    #
    # @note This method should be implemented in the Scene subclass.
    #
    # @param [Scene] old_scene this is the instance of the scene that is being moved
    #   away from.
    #
    def prepare_transition_from(old_scene) ; end

    include Draws

    #
    # When an actor is defined, through the class method `draw` a getter and setter method
    # is defined. However, it is a better interface internally not to rely heavily on send
    # and have this small amount of obfuscation in the event that this needs to change.
    #
    # @return the actor with the given name.
    #
    def actor(actor_or_actor_name)
      if actor_or_actor_name.is_a? String or actor_or_actor_name.is_a? Symbol
        send(actor_or_actor_name)
      else
        actor_or_actor_name
      end
    end

    #
    # Post a custom notification event. This will trigger an event for all the
    # objects that are registered for notification with the current state.
    #
    def notification(event,sender=nil)
      sender = sender || UnknownSender
      state.fire_events_for_notification(event,sender)
    end

    #
    # A scene has events which it will register when the window is established.
    #
    include HasEvents

    #
    # A scene defines animations which it will execute when the scene starts
    #
    include HasAnimations

    #
    # Allow the definition of a updater that will be executed when the scene starts.
    #
    # @example Setting up an event to 2 seconds after the scene has started.
    #
    #     class ExampleScene
    #
    #       draws :title
    #
    #       after 2.seconds do
    #         transition_to :next_scene
    #       end
    #     end
    #
    def self.after(ticks,&block)
      after_intervals.push AfterIntervalFactory.new ticks, &block
    end

    #
    # Perform an operation after the specified interval.
    #
    #     class ExampleScene
    #
    #       draws :player
    #
    #       def update
    #         if player.is_dead?
    #           after 2.seconds do
    #             transition_to :game_over
    #           end
    #         end
    #       end
    #
    #     end
    #
    def after(ticks,&block)
      tick = OnUpdateOperation.new interval: ticks, context: self
      tick.on_complete(&block)
      enqueue tick
    end

    #
    # Setups up the Actors for the Scene based on the ModelFactories that have been
    # defined.
    #
    # @note this method should not be overriden, otherwise the actors will perish!
    # @see #after_initialize
    #
    def initialize
      add_actors_to_scene
      after_initialize
    end

    def add_actors_to_scene
      self.class.actors.each do |scene_actor|
        actor_instance = scene_actor.create
        actor_instance.scene = self
        send "#{scene_actor.name}=", actor_instance
      end
    end

    #
    # The window is the main instance of the game. Using window can access a lot of
    # underlying Metro::Window, a subclass of Gosu::Window, that the Scene class is
    # obfuscating.
    #
    # @see Metro::Window
    # @see Gosu::Window
    #
    attr_reader :window

    #
    # Setting the window places the scene within in the specified window. Which
    # will cause a number of variables and settings to be set up. The {#show}
    # method is called after the window has been set.
    #
    def window=(window)
      @window = window

      state.window = window
      state.clear

      register_events!
      register_actors!
      register_animations!
      register_after_intervals!

      show
    end

    #
    # Register all the events that were defined for this scene.
    #
    def register_events!
      register_events_for_target(self,self.class.events)
    end

    #
    # Register all the actors that were defined for this scene.
    #
    def register_actors!
      self.class.actors.each { |actor| register_actor(actor) }
    end

    #
    # Register all the animations that were defined for this scene.
    #
    def register_animations!
      self.class.animations.each do |animation|
        animate animation.actor, animation.options, &animation.on_complete_block
      end
    end

    def register_after_intervals!
      self.class.after_intervals.each do |after_interval|
        after after_interval.ticks, &after_interval.block
      end
    end

    #
    # Registering an actor involves setting up the actor within
    # the window, adding them to the list of things that need to be
    # drawn and then registering any eventst that they might have.
    #
    def register_actor(actor_factory)
      registering_actor = actor(actor_factory.name)
      registering_actor.window = window
      registering_actor.show

      drawers.push(registering_actor)
      updaters.push(registering_actor)

      register_events_for_target(registering_actor,registering_actor.class.events)
    end

    #
    # Allows you to set or retrieve the scene name for the Scene.
    #
    # @example Retrieving the default scene name
    #
    #     class ExampleScene < GameScene
    #     end
    #
    #     ExampleScene.scene_name # => "example"
    #
    # @example Setting a custom name for the Scene
    #
    #     class RollingCreditsScene < GameScene
    #       scene_name "credits"
    #     end
    #
    #     RollingCreditsScene.scene_name # => "credits"
    #
    # @param [String] scene_name when specified it will set the scene name for the class
    #   to the value specified.
    #
    # @return the String name of the scene which it can be used as a reference for transitioning
    #   or for generating the appropriate view information.
    #
    def self.scene_name(scene_name=nil)
      @scene_name ||= begin
        if to_s == "Metro::Scene"
          to_s.underscore
        else
          to_s.gsub(/_?Scene$/i,'').underscore
        end
      end

      scene_name ? @scene_name = scene_name.to_s : @scene_name
    end

    #
    # @return a common name that can be used through the system as a common identifier.
    #
    def self.metro_name
      scene_name
    end

    #
    # @return an array of all the scene names of all the ancestor scenes
    #
    def self.hierarchy
      ancestors.find_all {|a| a.respond_to? :metro_name }.map(&:metro_name)
    end

    #
    # Allows you to set or retrieve the scene name for the Scene.
    #
    # @example Retrieving the default scene name
    #
    #     class ExampleScene
    #       def show
    #         puts "Showing Scene: #{self.scene_name}" # => Showing Scene: example
    #       end
    #     end
    #
    # @return the string name of the Scene.
    #
    def scene_name
      self.class.scene_name
    end

    #
    # @return the string representation of a scene, this is used for debugging.
    #
    def to_s
      "[SCENE: #{self.class.scene_name}(#{self.class})]"
    end

    #
    # Captures all classes that subclass Scene.
    #
    def self.inherited(base)
      scenes << base.to_s
      Scenes.add(base)
    end

    #
    # All subclasses of Scene, this should be all the defined scenes within the game.
    #
    # @return an Array of Scene subclasses
    #
    def self.scenes
      @scenes ||= []
    end

    #
    # Enqueue will add an updater to the list of updaters that are run initially when
    # update is called. An updater is any object that can respond to #update. This
    # is used for animations.
    #
    def enqueue(updater)
      updaters.push(updater)
    end

    #
    # The class defined updaters which will be converted to instance updaters when the scene
    # has started.
    #
    def self.after_intervals
      @after_intervals ||= []
    end

    #
    # The objects that need to be executed on every update. These objects are traditionally
    # animations or window events for held pressed buttons. But can be any objects that responds
    # to the method #update.
    #
    def updaters
      @updaters ||= []
    end

    #
    # The `base_update` method is called by the Game Window. This is to allow for any
    # special update needs to be handled before calling the traditional `update` method
    # defined in the subclassed Scene.
    #
    def base_update
      updaters.each { |updater| updater.update }
      update
      updaters.reject! { |updater| updater.update_completed? }
    end

    #
    # The objects that need to be drawn with every draw cycle. These objects are traditionally
    # the model objects, like the actors defined within the scene.
    #
    def drawers
      @drawers ||= []
    end

    #
    # The `base_draw` method is called by the Game Window. This is to allow for any
    # special drawing needs to be handled before calling the traditional `draw` method
    # defined in the subclassed Scene.
    #
    def base_draw
      drawers.each { |drawer| drawer.draw }
      draw
      drawers.reject! { |drawer| drawer.draw_completed? }
    end

    # This provides the functionality for view handling.
    include SceneView

    #
    # `transition_to` performs the work of transitioning this scene
    # to another scene.
    #
    # @param [String,Symbol,Object] scene_or_scene_name the name of the Scene which can
    #   be either the class or a string/symbol representation of the shortened scene name.
    #   This could also be an instance of scene.
    #
    def transition_to(scene_or_scene_name,options = {})
      new_scene = Scenes.generate(scene_or_scene_name,options)
      _prepare_transition(new_scene)
      window.scene = new_scene
    end

    #
    # Before a scene is transitioned away from to a new scene, this private method is
    # here to allow for any housekeeping or other work that needs to be done before
    # calling the subclasses implementation of `prepare_transition`.
    #
    # @param [Scene] new_scene this is the instance of the scene that is about to replace
    #   the current scene.
    #
    def _prepare_transition(new_scene)
      log.debug "Preparing to transition from scene #{self} to #{new_scene}"

      new_scene.class.actors.find_all {|actor_factory| actor_factory.load_from_previous_scene? }.each do |actor_factory|
        new_actor = new_scene.actor(actor_factory.name)
        current_actor = actor(actor_factory.name)
        new_actor._load current_actor._save
      end

      prepare_transition_to(new_scene)
      new_scene.prepare_transition_from(self)
    end

    #
    # Helper method that is used internally to setup the events for the specified target.
    #
    # @param [Object] target the intended target for the specified events. This object
    #   will have the appropriate methods and functionality to respond appropriately
    #   to the action blocks defined in the methods.
    #
    # @param [Array<EventFactory>] events an array of EventFactory objects that need to now
    #   be mapped to the specified target.
    #
    def register_events_for_target(target,events)
      state.add_events_for_target(target,events)
    end

    #
    # The event state manager is configured through the {#events} method, which
    # stores all the gamepad and keyboard events defined. By default a scene is
    # placed in the default state and events that are added to this basic state.
    #
    # @see Events
    #
    def state
      @event_state_manager ||= EventStateManager.new
    end

    #
    # A Scene represented as a hash currently only contains the drawers
    #
    # @return a hash of all the drawers
    #
    def to_hash
      drawn = drawers.find_all{|draw| draw.saveable_to_view }.inject({}) do |hash,drawer|
        drawer_hash = drawer.to_hash
        hash.merge drawer_hash
      end

      drawn
    end

  end
end