Game State Management
From PyWiki
Ok I'm going to skip the very basics here, I'm assuming you know the basics of how python-ogre works.
This page is intended to give an overview of how game states can be managed at runtime during the default render loop. The code here is not what we use in the final game but should give a basic overview of how the system works.
My method of going about this management is based heavily on the following articles: Managing Game States with OGRE and Managing Game States in C++
Contents |
Singleton Metaclass
I'm not going to go into a great deal of detail here regarding this other than giving the code required as I believe it will distract from the rest of the article, basically think of a singleton as an object witch can only be instantiated once during an applications runtime.
In python we can achieve this by the use of a metaclass which will override the default call behaviour of classes using it.
class Singleton(type): def __init__(cls,name,bases,dic): super(Singleton,cls).__init__(name,bases,dic) cls.instance=None def __call__(cls,*args,**kw): if cls.instance is None: cls.instance=super(Singleton,cls).__call__(*args,**kw) return cls.instance
If you want some more information about how this works take a look at the page on the Bloop wiki here
Abstract GameState Class
Lets start by taking a look at the abstract class the game states will inherit from:
class GameState(object): def __init__(self): pass def enter(self): pass def exit(self): pass def pause(self): pass def resume(self): pass def keyPressed(self, evt): pass def keyReleased(self, evt): pass def frameStarted(self, evt): return True def frameEnded(self, evt): return True def changeState(self): pass def pushState(self): pass def popState(self): pass
As you can see these are all empty methods with the exception of the frameStarted and frameEnded ones, the reason for this is that the outcome of these methods will be passes to the main frame listener of the application. If a frame listener was to return False then OGRE would exit.
We'll look at the game states themselves later, now all you need to know about is this base.
InputManager Class
This class is going to be a very simple key listener which will forward events to our GameManager for processing
class InputManager(OIS.KeyListener): def __init__(self, manager, rwindow): OIS.KeyListener.__init__(self) windowHandle = rwindow.getCustomAttributeInt("WINDOW") paramList = [("WINDOW", str(windowHandle))] inputManager = OIS.createPythonInputSystem(paramList) self.keyboard = inputManager \ .createInputObjectKeyboard(OIS.OISKeyboard, True) self.keyboard.setEventCallback(self) self.manager = manager def keyPressed(self, evt): self.manager.keyPressed(evt) def keyReleased(self, evt): self.manager.keyReleased(evt)
This class takes in the GameManager instance itself, as you can see it forwards keyPressed and keyReleased events to it, and the games render window. Notice also the call to the unbound __init__ method of the parent class. This is required to instantiate several KeyListener attributes we may use in this input manager.
The keyboard object self.keyboard is also make accessible outside this InputManager class so that we may use Buffered or Unbuffered input in the game.
GameFrameListener Class
This class will act as the frame listener for the application, as with the InputManager, this class will also take in an instance of the game manager which it will forward the frame events to.
class GameFrameListener(ogre.FrameListener): def __init__(self, gameManager): ogre.FrameListener.__init__(self) self.gameManager = gameManager def frameStarted(self, evt): successfulFrame = self.gameManager.frameStarted(evt) return successfulFrame def frameEnded(self, evt): successfulFrame = self.gameManager.frameEnded(evt) return successfulFrame
We will see later how this frame listener is instantiated later, its functionality should be fairly obvious, frame events, when caught are forwarded to the game manager. Again note the call to the parent classes __init__ method.
The GameManager Class
This GameManager class will handle the switching of game states without ever needing to know about the state transitions.
Here's the code:
class GameManager(object): __metaclass__ = Singleton def __init__(self): self.root = 0 self.inputManager = 0 self.states = [] def start(self, state): self.root = ogre.Root() self.defineResources() self.setupRenderSystem() self.frameListener = GameFrameListener(self) self.root.addFrameListener(self.frameListener) self.inputManager = InputManager(self, self.RenderWindow) self.changeState(state()) self.root.startRendering() def changeState(self, state): if len(self.states): oldState = self.states.pop() oldState.exit() self.states.append(state) state.enter() def pushState(self, state): if len(self.states): self.states[-1].pause() self.states.append(state) state.enter() def popState(self): if len(self.states): oldState = self.states.pop() oldState.exit() if len(self.states): self.states[-1].resume() def keyPressed(self, evt): self.states[-1].keyPressed(evt) def keyReleased(self, evt): self.states[-1].keyReleased(evt) def frameStarted(self, evt): self.inputManager.keyboard.capture() sf = self.states[-1].frameStarted(evt) return sf def frameEnded(self, evt): sf = self.states[-1].frameEnded(evt) return sf def setupRenderSystem(self): if not self.root.restoreConfig() and not self.root.showConfigDialog(): raise Exception("User cancelled config dialogue") self.RenderWindow = self.root.initialise(True, "GameWindow") ogre.ResourceGroupManager.getSingleton().initialiseAllResourceGroups() def defineResources(self): conf = ogre.ConfigFile() conf.load("resources.cfg") seci = conf.getSectionIterator() while seci.hasMoreElements(): sectionName = seci.peekNextKey() settings = seci.getNext() for item in settings: typeName = item.key archName = item.value ogre.ResourceGroupManager.getSingleton().addResourceLocation \ (archName, typeName, sectionName)
I'm going to assume you understand the operation of the defineResources and setupRenderSystem methods and wont say much about them other than to make sure you call them in the correct order (I was driven mad for hours wondering why my meshes would load but their materials wouldn't).
Lets take a look at the other methods one at a time. First __init__ nothing much happens in this method other than we define an empty root and inputManager objects as well as an empty list states, which will serve as our game states stack.
The start method is a lot more interesting. As you can see it takes in a game state object (we'll see later how this is done), that represents the starting state of the game.
This method basically handles the whole startup of OGRE also the creation of the frame listener and input manager. We pass self to both so that they can forward the caught events back to the game manager. We call the changeState method passing in(and calling) the starting state. after this the render loop is stated and everyone is happy :)
Before we look at the state change methods a quick word about how we handle the game states. As you have already seen we have a list states in our game manager. This list will act as a stack of states allowing us to keep track of which state the game is currently in and the previous order of states which lead to this current state. This sounds complicated but its not. The current state is always on top of the stack, any state which it moves to is pushed on top of the stack and when the state is done it pops off the top of the stack and the previous state (now at the top) resumes.
Lets have a look at the changeState method then. This takes in the state we want to change to. First it checks if the state stack contains any states, if so it removes the top state from the stack and calls its .exit() method, shutting down any operation of this state. it then pushes the state taken in to the top of the state stack and calls its .enter() method.
The popState method operates in a similar way, it removed the top state from the stack, calls its exit method but then calls the resume method of the state now on top of the stack.
pushState is a little different, this takes in a state but rather than exiting the state stack top, the top states pause method is called before the new state is pushes to the stack top and has its enter method called.
I wont go over the operation of the other methods as they should be pretty self explanitory, the key and frame event methods are simply forwarded to the state currently on top of the state stack.
The Game States
The 'game' is really only going to be a demonstration of the game state switching, basically all you will see is a coloured screen based on the current state, Red for the Intro state, Blue for the Play state and green for the Pause state.
The game will have the following controls in each state:
- Intro: Space 'starts' the game and sends you to the play state. ESC quits the game
- Play: P Pauses the game, sending you to the pause state. ESC sends you back to the Intro state.
- Pause: P Unpauses the game, sending you back to the Play state.
IntroState
Like all of the game states, this one inherits from GameState and uses the Singleton metaclass.
class IntroState(GameState): __metaclass__ = Singleton def enter(self): self.root = ogre.Root.getSingleton() self.sceneManager = self.root.createSceneManager(ogre.ST_GENERIC) self.camera = self.sceneManager.createCamera("IntroCamera") self.viewport = self.root.getAutoCreatedWindow().addViewport(self.camera) self.viewport.setBackgroundColour(ogre.ColourValue(1.0, 0.0, 0.0)) self.ExitGame = False def exit(self): self.sceneManager.clearScene() self.sceneManager.destroyAllCameras() self.root.getAutoCreatedWindow().removeAllViewports() def keyPressed(self, evt): if evt.key == OIS.KC_SPACE: GameManager().changeState(PlayState()) elif evt.key == OIS.KC_ESCAPE: self.ExitGame = True def frameEnded(self, evt): return not self.ExitGame
Lets go over the methods in this game state:
First enter, this method would be called whenever the scene is entered (who would have guessed). The method first aquires the ogre Root object then creates a sceneManager, camera and viewport. Also the ExitGame variable is created and set to False.
The exit method clears the scene, destroys the cameras and removes the viewports. Nice :)
As you can see the keyPressed method is called from the game manager every time a key event is forwarded from the InputManager (ie buffered input) here all we do is check which key is being pressed, in the event of the escape key being pressed ExitGame is set to true.
Should the space key be pressed the state is changed to the PlayState. We do this, not by having a local reference to the GameManager, but rather since the GameManager is also a singleton, we can acquire the GameManagers instance directly then call the changeState method and pass in the state we wish to change to using:
GameManager().changeState(PlayState())
The frameEnded event is another event which would be called after event forwarding from the GameManager, in this case it simply checks if ExitGame is set, and if it is returns False (remember if a frame listener returns false from a frame event OGRE shuts down).
PlayState
class PlayState(GameState): __metaclass__ = Singleton def enter(self): self.root = ogre.Root.getSingleton() self.sceneManager = self.root.createSceneManager(ogre.ST_GENERIC) self.camera = self.sceneManager.createCamera("PlayCamera") self.viewport = self.root.getAutoCreatedWindow().addViewport(self.camera) self.viewport.setBackgroundColour(ogre.ColourValue(0.0, 0.0, 1.0)) def exit(self): self.sceneManager.clearScene() self.sceneManager.destroyAllCameras() self.root.getAutoCreatedWindow().removeAllViewports() def resume(self): self.viewport.setBackgroundColour(ogre.ColourValue(0.0, 0.0, 1.0)) def keyPressed(self, evt): if evt.key == OIS.KC_P: GameManager().pushState(PauseState()) elif evt.key == OIS.KC_ESCAPE: GameManager().changeState(IntroState())
I'm not going to go over this entire class, since it is very similar to the IntroState. All that really requires mentioning is the way we change states. If we are going to the Pause state (P key) we need to be able to resume the play state at some point, so we use the pushState method, since this pauses the current state and appends the pause state to the top of the state stack. However when we exit back to the Intro state we no longer need this current state so we call the changeState method and go back to the IntroState.
PauseState
The final state used here is the PauseState the code for which is shown below (there should really be a proper exit method here too):
class PauseState(GameState): __metaclass__ = Singleton def enter(self): self.root = ogre.Root.getSingleton() self.viewport = self.root.getAutoCreatedWindow().getViewport(0) self.viewport.setBackgroundColour(ogre.ColourValue(0.0, 1.0, 0.0)) def keyPressed(self, evt): if evt.key == OIS.KC_P: GameManager().popState()
Again most of what we have here has already been covered, all we will talk about is the exiting of the pause state.
As you may remember from the Play state that when we enter the pause state, the play state is first paused and the Pause state is pushed to the top of the state stack and entered. So to exit the pause state and resume the previous(Play) state we simply call the GameManagers popState method.
Putting it all together
To put all this together couldn't be simpler. all we need is a 'main' function like the one below:
if __name__ == "__main__": game = GameManager() game.start(IntroState)
As you can see, all we need to do here is instantiate the GameManager and call its start method, passing in the game state you wish to start in.
Possible Problems
With the above code all classes are placed in the same module, certain methods of seperating out the classes in to a more realistic module structure can lead to import loop problems. The article Resolving Import Loops and its discussion page from the Bloop wiki, give examples of how to resolve these problems.
