/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

  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. module Chingu
  22. #
  23. # GameStateManger is responsible for keeping track of game states with a simple pop/push stack.
  24. #
  25. # More about the concept of states in games:
  26. # http://gamedevgeek.com/tutorials/managing-game-states-in-c/
  27. # http://www.gamedev.net/community/forums/topic.asp?topic_id=477320
  28. #
  29. # Chingu::Window automatically creates a @game_state_manager and makes it accessible in our game loop.
  30. # By default the game loop calls update, draw, button_up(id) and button_down(id) on the active state.
  31. #
  32. # ==== Chingu Examples
  33. #
  34. # Enter a new game state, Level, don't call finalize() on the game state we're leaving.
  35. # push_game_state(Level, :finalize => false)
  36. #
  37. # Return to the previous game state, don't call setup() on it when it becomes active.
  38. # pop_game_state(:setup => false)
  39. #
  40. # If you want to use Chingus GameStateManager _without_ Chingu::Window, see example5.rb
  41. #
  42. class GameStateManager
  43. attr_accessor :inside_state
  44. def initialize
  45. @inside_state = nil
  46. @game_states = []
  47. @transitional_game_state = nil
  48. @transitional_game_state_options = {}
  49. end
  50. #
  51. # Gets the currently active gamestate (top of stack)
  52. #
  53. def current_game_state
  54. @game_states.last
  55. end
  56. alias :current current_game_state
  57. #
  58. # Returns all gamestates with currenlty active game state on top.
  59. #
  60. def game_states
  61. @game_states.reverse
  62. end
  63. #
  64. # Sets a game state to be called between the old and the new game state
  65. # whenever a game state is switched,pushed or popped.
  66. #
  67. # The transitional game state is responsible for switching to the "new game state".
  68. # It should do so with ":transitional => false" not to create an infinite loop.
  69. #
  70. # The new game state is the first argument to the transitional game states initialize().
  71. #
  72. # Example:
  73. # transitional_game_state(FadeIn)
  74. # push_game_state(Level2)
  75. #
  76. # would in practice become:
  77. #
  78. # push_game_state(FadeIn.new(Level2))
  79. #
  80. # This would be the case for every game state change until the transitional game state is removed:
  81. # transitional_game_state(nil) # or false
  82. #
  83. # Very useful for fading effect between scenes.
  84. #
  85. def transitional_game_state(game_state, options = {})
  86. @transitional_game_state = game_state
  87. @transitional_game_state_options = options
  88. end
  89. #
  90. # Switch to a given game state, _replacing_ the current active one.
  91. # By default setup() is called on the game state we're switching _to_.
  92. # .. and finalize() is called on the game state we're switching _from_.
  93. #
  94. def switch_game_state(state, options = {})
  95. options = {:setup => true, :finalize => true, :transitional => true}.merge!(options)
  96. # Don't setup or finalize the underlying state, since it never becomes active.
  97. pop_game_state(options.merge(:setup => false))
  98. push_game_state(state, options.merge(:finalize => false))
  99. end
  100. alias :switch :switch_game_state
  101. #
  102. # Adds a state to the game state-stack and activates it.
  103. # By default setup() is called on the new game state
  104. # .. and finalize() is called on the game state we're leaving.
  105. #
  106. def push_game_state(state, options = {})
  107. options = {:setup => true, :finalize => true, :transitional => true}.merge(options)
  108. new_state = game_state_instance(state)
  109. if new_state
  110. # So BasicGameObject#create connects object to new state in its setup()
  111. self.inside_state = new_state
  112. # Make sure the game state knows about the manager
  113. # Is this doubled in GameState.initialize() ?
  114. new_state.game_state_manager = self
  115. # Call setup
  116. new_state.setup if new_state.respond_to?(:setup) && options[:setup]
  117. # Give the soon-to-be-disabled state a chance to clean up by calling finalize() on it.
  118. current_game_state.finalize if current_game_state.respond_to?(:finalize) && options[:finalize]
  119. if @transitional_game_state && options[:transitional]
  120. # If we have a transitional, push that instead, with new_state as first argument
  121. transitional_game_state = @transitional_game_state.new(new_state, @transitional_game_state_options)
  122. transitional_game_state.game_state_manager = self
  123. self.push_game_state(transitional_game_state, :transitional => false)
  124. else
  125. # Push new state on top of stack and therefore making it active
  126. @game_states.push(new_state)
  127. end
  128. ## MOVED: self.inside_state = current_game_state
  129. end
  130. self.inside_state = nil # no longer 'inside' (as in within initialize() etc) a game state
  131. end
  132. alias :push :push_game_state
  133. #
  134. # Pops a state off the game state-stack, activating the previous one.
  135. # By default setup() is called on the game state that becomes active.
  136. # .. and finalize() is called on the game state we're leaving.
  137. #
  138. def pop_game_state(options = {})
  139. options = {:setup => true, :finalize => true, :transitional => true}.merge(options)
  140. #
  141. # Give the soon-to-be-disabled state a chance to clean up by calling finalize() on it.
  142. #
  143. current_game_state.finalize if current_game_state.respond_to?(:finalize) && options[:finalize]
  144. #
  145. # Activate the game state "bellow" current one with a simple Array.pop
  146. #
  147. @game_states.pop
  148. # So BasicGameObject#create connects object to new state in its setup()
  149. # Is this doubled in GameState.initialize() ?
  150. self.inside_state = current_game_state
  151. # Call setup on the new current state
  152. current_game_state.setup if current_game_state.respond_to?(:setup) && options[:setup]
  153. if @transitional_game_state && options[:transitional]
  154. # If we have a transitional, push that instead, with new_state as first argument
  155. transitional_game_state = @transitional_game_state.new(current_game_state, @transitional_game_state_options)
  156. transitional_game_state.game_state_manager = self
  157. self.push_game_state(transitional_game_state, :transitional => false)
  158. end
  159. ## MOVED: self.inside_state = current_game_state
  160. self.inside_state = nil # no longer 'inside' (as in within initialize() etc) a game state
  161. end
  162. alias :pop :pop_game_state
  163. #
  164. # Returns the previous game state. Shortcut: "previous"
  165. #
  166. def previous_game_state
  167. current_game_state.previous_game_state
  168. end
  169. alias :previous previous_game_state
  170. #
  171. # Remove all game states from stack. Shortcut: "clear"
  172. #
  173. def clear_game_states
  174. @game_states.clear
  175. self.inside_state = nil
  176. end
  177. alias :clear :clear_game_states
  178. #
  179. # Pops through all game states until matching a given game state (takes either a class or instance to match).
  180. #
  181. def pop_until_game_state(new_state, options = {})
  182. if new_state.is_a? Class
  183. raise ArgumentError, "No state of given class is on the stack" unless @game_states.any? {|s| s.is_a? new_state }
  184. pop_game_state(options) until current_game_state.is_a? new_state
  185. else
  186. raise ArgumentError, "State is not on the stack" unless @game_states.include? new_state
  187. pop_game_state(options) until current_game_state == new_state
  188. end
  189. end
  190. #
  191. # This method should be called from button_down(id) inside your main loop.
  192. # Enables the game state manager to call button_down(id) on active game state.
  193. #
  194. # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
  195. #
  196. def button_down(id)
  197. current_game_state.button_down(id) if current_game_state
  198. end
  199. #
  200. # This method should be called from button_up(id) inside your main loop.
  201. # Enables the game state manager to call button_up(id) on active game state.
  202. #
  203. # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
  204. #
  205. def button_up(id)
  206. current_game_state.button_up(id) if current_game_state
  207. end
  208. #
  209. # This method should be called from update() inside your main loop.
  210. # Enables the game state manager to call update() on active game state.
  211. #
  212. # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
  213. #
  214. def update(options = {})
  215. puts current_game_state.to_s if options[:debug]
  216. current_game_state.update_trait if current_game_state
  217. current_game_state.update if current_game_state
  218. end
  219. #
  220. # This method should be called from draw() inside your main loop.
  221. # Enables the game state manager to call update() on active game state.
  222. #
  223. # If you're using Chingu::Window instead of Gosu::Window this will automaticly be called.
  224. #
  225. def draw
  226. if current_game_state
  227. current_game_state.draw_trait
  228. current_game_state.draw
  229. end
  230. end
  231. private
  232. #
  233. # Returns a GameState-instance from either a GameState class or GameState-object
  234. #
  235. def game_state_instance(state)
  236. new_state = state
  237. #
  238. # If state is a GameState-instance, just queue it.
  239. # If state is a GameState-class, instance it.
  240. #
  241. new_state = state.new if state.is_a? Class
  242. return new_state # if new_state.kind_of? Chingu::GameState # useless check.
  243. end
  244. end
  245. end