PageRenderTime 31ms CodeModel.GetById 6ms app.highlight 22ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/chingu/game_state_manager.rb

http://github.com/ippa/chingu
Ruby | 283 lines | 102 code | 38 blank | 143 comment | 22 complexity | 7357efab0787444e85db2743aba89bc6 MD5 | raw file
Possible License(s): LGPL-2.1
  1#--
  2#
  3# Chingu -- OpenGL accelerated 2D game framework for Ruby
  4# Copyright (C) 2009 ippa / ippa@rubylicio.us
  5#
  6# This library is free software; you can redistribute it and/or
  7# modify it under the terms of the GNU Lesser General Public
  8# License as published by the Free Software Foundation; either
  9# version 2.1 of the License, or (at your option) any later version.
 10#
 11# This library is distributed in the hope that it will be useful,
 12# but WITHOUT ANY WARRANTY; without even the implied warranty of
 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 14# Lesser General Public License for more details.
 15#
 16# You should have received a copy of the GNU Lesser General Public
 17# License along with this library; if not, write to the Free Software
 18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 19#
 20#++
 21
 22
 23module Chingu
 24  #
 25  # GameStateManger is responsible for keeping track of game states with a simple pop/push stack.
 26  #
 27  # More about the concept of states in games:
 28  # http://gamedevgeek.com/tutorials/managing-game-states-in-c/
 29  # http://www.gamedev.net/community/forums/topic.asp?topic_id=477320
 30  #
 31  # Chingu::Window automatically creates a @game_state_manager and makes it accessible in our game loop.
 32  # By default the game loop calls update, draw, button_up(id) and button_down(id) on the active state.
 33  #
 34  # ==== Chingu Examples
 35  #
 36  # Enter a new game state, Level, don't call finalize() on the game state we're leaving.
 37  #   push_game_state(Level, :finalize => false)
 38  #
 39  # Return to the previous game state, don't call setup() on it when it becomes active.
 40  #   pop_game_state(:setup => false)
 41  #
 42  # If you want to use Chingus GameStateManager _without_ Chingu::Window, see example5.rb
 43  #
 44  class GameStateManager
 45    attr_accessor :inside_state
 46    
 47    def initialize
 48      @inside_state = nil
 49      @game_states = []
 50      @transitional_game_state = nil
 51      @transitional_game_state_options = {}
 52    end
 53    
 54    #
 55    # Gets the currently active gamestate (top of stack)
 56    #
 57    def current_game_state
 58      @game_states.last
 59    end
 60    alias :current current_game_state
 61
 62    #
 63    # Returns all gamestates with currenlty active game state on top.
 64    #
 65    def game_states
 66      @game_states.reverse
 67    end
 68    
 69    #
 70    # Sets a game state to be called between the old and the new game state 
 71    # whenever a game state is switched,pushed or popped.
 72    #
 73    # The transitional game state is responsible for switching to the "new game state".
 74    # It should do so with ":transitional => false" not to create an infinite loop.
 75    # 
 76    # The new game state is the first argument to the transitional game states initialize().
 77    #
 78    # Example:
 79    #   transitional_game_state(FadeIn)
 80    #   push_game_state(Level2)
 81    #
 82    # would in practice become:
 83    # 
 84    #   push_game_state(FadeIn.new(Level2))
 85    #
 86    # This would be the case for every game state change until the transitional game state is removed:
 87    #   transitional_game_state(nil)  # or false
 88    #
 89    # Very useful for fading effect between scenes.
 90    #
 91    def transitional_game_state(game_state, options = {})
 92      @transitional_game_state = game_state
 93      @transitional_game_state_options = options
 94    end
 95    
 96    #
 97    # Switch to a given game state, _replacing_ the current active one.
 98    # By default setup() is called on the game state  we're switching _to_.
 99    # .. and finalize() is called on the game state we're switching _from_.
100    #   
101    def switch_game_state(state, options = {})
102      options = {:setup => true, :finalize => true, :transitional => true}.merge!(options)
103      
104      # Don't setup or finalize the underlying state, since it never becomes active.
105      pop_game_state(options.merge(:setup => false))
106      push_game_state(state, options.merge(:finalize => false))
107    end
108    alias :switch :switch_game_state
109    
110    #
111    # Adds a state to the game state-stack and activates it.
112    # By default setup() is called on the new game state 
113    # .. and finalize() is called on the game state we're leaving.
114    #
115    def push_game_state(state, options = {})
116      options = {:setup => true, :finalize => true, :transitional => true}.merge(options)
117      
118      new_state = game_state_instance(state)
119            
120      if new_state
121        
122        # So BasicGameObject#create connects object to new state in its setup()
123        self.inside_state = new_state
124        
125        # Make sure the game state knows about the manager
126        # Is this doubled in GameState.initialize() ?
127        new_state.game_state_manager = self
128        
129        # Call setup
130        new_state.setup               if new_state.respond_to?(:setup) && options[:setup]
131                
132        # Give the soon-to-be-disabled state a chance to clean up by calling finalize() on it.
133        current_game_state.finalize   if current_game_state.respond_to?(:finalize) && options[:finalize]
134        
135        if @transitional_game_state && options[:transitional]
136          # If we have a transitional, push that instead, with new_state as first argument
137          transitional_game_state = @transitional_game_state.new(new_state, @transitional_game_state_options)
138          transitional_game_state.game_state_manager = self
139          self.push_game_state(transitional_game_state, :transitional => false)
140        else
141          # Push new state on top of stack and therefore making it active
142          @game_states.push(new_state)
143        end
144        ## MOVED: self.inside_state = current_game_state
145      end
146      
147      self.inside_state = nil   # no longer 'inside' (as in within initialize() etc) a game state
148    end
149    alias :push :push_game_state
150    
151    #
152    # Pops a state off the game state-stack, activating the previous one.
153    # By default setup() is called on the game state that becomes active.
154    # .. and finalize() is called on the game state we're leaving.
155    #
156    def pop_game_state(options = {})
157      options = {:setup => true, :finalize => true, :transitional => true}.merge(options)
158      
159      #
160      # Give the soon-to-be-disabled state a chance to clean up by calling finalize() on it.
161      #
162      current_game_state.finalize    if current_game_state.respond_to?(:finalize) && options[:finalize]
163
164      #
165      # Activate the game state "bellow" current one with a simple Array.pop
166      #
167      @game_states.pop
168      
169      # So BasicGameObject#create connects object to new state in its setup()
170      # Is this doubled in GameState.initialize() ?
171      self.inside_state = current_game_state
172      
173      # Call setup on the new current state
174      current_game_state.setup       if current_game_state.respond_to?(:setup) && options[:setup]
175      
176      if @transitional_game_state && options[:transitional]
177        # If we have a transitional, push that instead, with new_state as first argument
178        transitional_game_state = @transitional_game_state.new(current_game_state, @transitional_game_state_options)
179        transitional_game_state.game_state_manager = self
180        self.push_game_state(transitional_game_state, :transitional => false)
181      end
182      
183      ## MOVED: self.inside_state = current_game_state
184      self.inside_state = nil   # no longer 'inside' (as in within initialize() etc) a game state
185    end
186    alias :pop :pop_game_state
187
188    #
189    # Returns the previous game state. Shortcut: "previous"
190    #
191    def previous_game_state
192      current_game_state.previous_game_state
193    end
194    alias :previous previous_game_state
195    
196    #
197    # Remove all game states from stack. Shortcut: "clear"
198    #
199    def clear_game_states
200      @game_states.clear
201      self.inside_state = nil
202    end
203    alias :clear :clear_game_states
204    
205    #
206    # Pops through all game states until matching a given game state (takes either a class or instance to match).
207    #    
208    def pop_until_game_state(new_state, options = {})
209      if new_state.is_a? Class
210        raise ArgumentError, "No state of given class is on the stack" unless @game_states.any? {|s| s.is_a? new_state }
211
212        pop_game_state(options) until current_game_state.is_a? new_state
213
214      else
215        raise ArgumentError, "State is not on the stack" unless @game_states.include? new_state
216
217        pop_game_state(options) until current_game_state == new_state
218      end
219    end
220        
221    #
222    # This method should be called from button_down(id) inside your main loop.
223    # Enables the game state manager to call button_down(id) on active game state.
224    #
225    # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
226    #
227    def button_down(id)
228      current_game_state.button_down(id) if current_game_state
229    end
230    
231    #
232    # This method should be called from button_up(id) inside your main loop.
233    # Enables the game state manager to call button_up(id) on active game state.
234    #
235    # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
236    #
237    def button_up(id)
238      current_game_state.button_up(id)  if current_game_state
239    end
240    
241    #
242    # This method should be called from update() inside your main loop.
243    # Enables the game state manager to call update() on active game state.
244    #
245    # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
246    #
247    def update(options = {})
248      puts current_game_state.to_s  if options[:debug]
249      current_game_state.update_trait if current_game_state
250      current_game_state.update       if current_game_state
251    end
252
253    #
254    # This method should be called from draw() inside your main loop.
255    # Enables the game state manager to call update() on active game state.
256    #
257    # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
258    #
259    def draw
260      if current_game_state
261        current_game_state.draw_trait
262        current_game_state.draw
263      end
264    end
265    
266    private
267    
268    #
269    # Returns a GameState-instance from either a GameState class or GameState-object
270    #
271    def game_state_instance(state)
272      new_state = state
273      #
274      # If state is a GameState-instance, just queue it.
275      # If state is a GameState-class, instance it.
276      #
277      new_state = state.new if state.is_a? Class
278      
279      return new_state  # if new_state.kind_of? Chingu::GameState # useless check.
280    end
281    
282  end
283end