Managing scenes: am I doing it wrong?
I'm working in Pygame, which doesn't manage scenes. This is something I've never really got the hang of. I've always gotten away with doing something really ad-hoc. Here's my plan for all the possible scenes in this game I'm working on (I call them contexts):The way I initially had it, the main game loop would call the current scene, which would then either return self, meaning keep the scene the same, or a new scene object, which then became the current scene. This runs into a circular import problem, for instance, when WorldMap, Action, and GameOver are in different modules. Finally what I settled on is a global stack managed by the Scene base class that I can push onto and pop off of. Instead of the main loop keeping track of the current scene, it checks the stack and uses the topmost scene. GameOver doesn't have to import WorldMap anymore, since it can just pop the stack twice.
I since found out that that's how Cocos does it, so it sounds like a good idea to me. But are there any shortcomings of this approach? It seems like it would still lead to circular import problems in certain situatons, but I think my particular web avoids that problem, but only because of luck.
I'm mostly curious if there's just a better way to do things that I haven't thought of yet.
(log in to comment)
Comments
I like that CutScene doesn't have to hang on to a reference to the Action it's above (which I had to do in an earlier version), since it's stored in the global stack.
One advantage of this over a central stack that the application manages is that the current scene has complete control of the scene below. There are assurances about the stacks that you will construct so there is less constraint on the Scenes to conform to a common API. PauseScene could tinker with the GameScene to whatever end you like (some visual effect, perhaps?), because it is never going to wrap a MapScene.
Circular dependencies are fairly easy to resolve in Python because you can import inside a function's scope if need be.
game menu -> level = director.push()
end of level = director.pop()
pause = director.push()
end pause = director.pop()
game level -> shop = director.push()
.. and another pop() :-)
game level -> next level = director.replace()
more complex I hear you say?
game menu (or level) -> cutscene -> level = director.replace(level), director.push(cutscene) .. play cutscene .. director.pop()
⌘P on OS X, ^P on Windows / Linux)
Also cocos has a built-in pause feature - just hit <accel>-P (ie. It's easy for a game state to switch to any other state without any complex tangled web of inter-dependencies. In a game state's update method simply call:
self.state_mgr.switch_state("new state")
Alternatively it can be designed to use a state stack:
self.state_mgr.push_state("new state") # Use new game state
self.state_mgr.pop_state() # Return to whatever state was previously used
This will have the state manager call lose_focus on the old state, switch the current state to the state found at the dictionary entry matching the given state name, then call gain_focus on that. Now when the game loop calls the game state manager's update and draw methods the game state manager will be calling the methods of this new current state.
* To me a scene might have collections of entities, background layers, etc. and would be owned by a game state (most likely the play game state, for obvious reasons.)
But I have a question. Do you literally call switch_state with a string, or do you call it with a reference to the state that you're switching to? If it's the string, how does that work? If it's the state, then I think you run into the same interdependency issue that I mentioned. You'll wind up with circular imports at some point, right? (Although I like mauve's solution of putting the imports inside a method, so it's not really a problem, I just don't see how your framework is any different in this respect.)
class GameStateManager:
def __init__(self):
self.game_states = {}
self.game_states["intro"] = GameStateIntro(self)
self.game_states["play"] = GameStatePlay(self)
self.game_states["pause"] = GameStatePause(self)
self.current_state = self.game_states["intro"]
Alternatively you could use the objects directly instead of a dictionary (which might not be a bad idea):
class GameStateManager:
def __init__(self):
self.play_state = GameStatePlay(self)
self.pause_state = GameStatePause(self)
# etc.
Then the game states could switch states with:
self.state_mgr.switch_state(self.state_mgr.pause_state)
Which seems a little more verbose.
What I mean to say is, if you used:
self.state_mgr.switch_state(self.state_mgr.pause_state)
Python would know what state_mgr is and that it has a pause_state even without importing the game state manager module. In this example, state_mgr is a reference to the game state manager the game state belongs to.
The game state manager module would import the modules for the pause state, play state, etc. (to create these objects), but these game states would not need to import the game state manager's module.
self.state_mgr.switch_state(GameStatePlay(self.state_mgr, level=6))
But that doesn't work if you can only pass the string "play".
self.state_mgr.switch_state("play_state", level=6)
with definition os switch_state like
def switch_state(self, state, *args, **kwargs):
self.current_state = self.game_states[state]
self.current_state.set_options(*args, **kwargs)
or if the state-dictionary just contain the classes like
self.game_states["intro"] = IntroState
then do something like this
self.current_state = self.game_states[state](*args, **kwargs)
That being said, for a pyweek game, they are probably fine.
There's no "going back to" Action or WorldMap when Pause exits; they were on the entire time. Pause's on_enter e.g. sets a time scale to 0, and its on_exit sets it back to 1.
So if you have more than one active state at a time, how do you choose which one draws to the screen and accepts input? Is this also done with global flags?
They all draw to the screen; they all accept input. Those obviously need to be a stack, but that can be handled by doing whatever input / render pushes are necessary in on_enter. Likewise you just draw back-to-front, and a state that doesn't want something else showing can paint over it. The renderer and input system have stacks (even without a state system, these will have stacks out of some necessity); the state system does not.
ikanreed on 2010/08/07 14:35:
Keep in mind that not all cutscenes are going to shove you back to your world map (unless they are). Consider retaining enough GameState info to jump back into the action after a cutscene.