[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[pygame] Game State Switching Example



I found a better way than I'd done before to switch between the various
states of a game, such as a main gameplay screen and a status screen.
Attached is some example code. Something for the Cookbook, maybe?
#!/usr/bin/python
## (A line needed for proper operation on Linux systems.)

"""
State Machine Demo

An example of one way to switch between the various states of a game,
such as a main gameplay screen and a status screen. There's just enough
detail to show how the idea works in practice with a fake title screen,
status screen, and a pop-up screen. You can see the number of items on
the stack flicker from 0 to 1, or 1 to 2, as you run the program.

Requirements: Python (python.org) and Pygame (pygame.org); both are free.
You could adapt this to work without Pygame if you want.

How it works:
The system switches between states by keeping a list called "game_state".
The Game class' main loop, Go(), looks at this list, pulls off the last item,
and tries to call a function named by it. This system helps prevent messy
infinite loops of function A calling B and B calling A, by having each screen
set the class' "done" flag to end its control of the program. When you want to
switch to another screen, just put the desired next screen(s) onto the stack.

Incidentally, this demo shows the shell of a way to run an interface. Have
several Interface classes that can be "installed" each time a game screen
starts and drawn in each loop, and have it call a Notify() function in the
game to leave messages in {dict} format. The game loops can then interpret
the messages however they want to. This method is an improvement over an
earlier version where there were multiple versions of Notify(), adding an
unnecessary layer of complexity. This interface concept isn't necessary
to implement the general idea of the state machine, though.
"""

__author__ = "Kris Schnee"
__version__ = "2007.7"
__license__ = "Public Domain"

import os ## Imported only for the following line.
os.environ["SDL_VIDEO_CENTERED"] = "1" ## Center the graphics window.
import pygame ## Graphics/event/etc. library based on SDL.
from pygame.locals import * ## Event handling constants.

SCREEN_SIZE = (800,600)
STARTING_SCREEN = "Title Screen"

class DummyInterface:
    """A class that can hold "widgets" like buttons and menus.

    It can react to Pygame events. It's not necessary to this demo,
    but shows one way of doing an interface."""

    def __init__(self,**options):
        self.owner = options.get("owner") ## The object that I Notify().

    def Draw(self):
        pass

    def HandleEvent(self,event):
        """React to a Pygame event.

        This is a dummy interface class that always returns False,
        indicating that the event was not handled and that the game's
        "screen" loop should handle it.

        For instance, a button that's clicked on can send a message to
        the Game object, eg.: {"headline":"clicked","what":"bOK"}
        This would be done by calling self.owner.Notify( [something] )."""
        return False


class Game:
    def __init__(self,**options):
        self.screen = options.get("screen",pygame.display.set_mode(SCREEN_SIZE))
        self.game_state = [STARTING_SCREEN] ## A stack of game screens.
        self.interface = None ## Optional interface object.
        self.done = False ## Done with current screen loop?
        self.messages = [] ## Checked during event loops.

        self.font = pygame.font.Font(None,24) ## For demo purposes.
        self.clock = pygame.time.Clock() ## For FPS managment.

    def Go(self):
        """This is the main loop of the game.

        Go between various screens until the game is over.
        There's a stack of game states in self.game_state, which can have
        additional items appended and popped. If the stack is empty, or
        if this loop reaches a state not corresponding to the name of a
        function this class has, the game will end.

        The exact way this determines the function names assumes that they're
        functions of this class, named exactly as the strings appended to
        the list, minus spaces. Eg. "Title Screen" leads this function to look
        for a function called TitleScreen. Just change the "naming convention"
        line if you dislike it. If at any point no appropriate function is
        found, or if nothing is left on the stack, the game ends."""
        while True:
            if not self.game_state:
                break ## Game over!
            
            next_screen = self.game_state.pop()
            print "Returned to main loop. Now switching to: "+next_screen
            function_name = next_screen.replace(" ","") ## Naming convention
            if hasattr(self,function_name):
                function = getattr(self,function_name)
                function()
            else:
                break ## Game over!
        print "Game over. Thanks for playing!"

    def Notify(self,message):
        """Get a message from the interface or game entities.

        These messages are handled during one of the screen loops.
        Use a dictionary format with a "headline" key, eg:
        {"headline":"Got Object","object":"Coconut"} """
        self.messages.append(message)

    def TitleScreen(self):
        """A sample screen."""
        self.interface = DummyInterface()

        self.done = False
        while not self.done:

            ## Handle messages from elsewhere, such as the interface.
            for message in self.messages:
                headline = message.get("headline","")
                print "Got a message: "+headline

            self.messages = []

            ## Handle events.
            for event in pygame.event.get():
                handled = self.interface.HandleEvent(event)
                if not handled:
                    if event.type == QUIT:
                        self.done = True
                    elif event.type == KEYDOWN:
                        if event.key == K_ESCAPE:
                            self.done = True

                        ## Some sample reaction to events.
                        elif event.key == K_s:
                            ## This screen will end and go to another screen.
                            self.done = True
                            self.game_state.append("Status Screen")

            ## Draw a sample set of stuff on the screen.
            self.screen.fill((0,180,180,255))
            text = "This is the Title Screen. Esc= quit, S= Status Screen."
            text_rendered = self.font.render(text,1,(255,255,255))
            self.screen.blit(text_rendered,(100,100))
            status = self.font.render("# of game states on stack: "+str(len(self.game_state)),1,(255,255,255))
            self.screen.blit(status,(100,300))
            self.interface.Draw()

            pygame.display.update()
            self.clock.tick(20)

    def StatusScreen(self):
        """A sample screen."""
        self.interface = DummyInterface()

        self.done = False
        while not self.done:

            ## Handle messages from elsewhere, such as the interface.
            for message in self.messages:
                headline = message.get("headline","")
                print "Got a message: "+headline

            self.messages = []

            ## Handle events.
            for event in pygame.event.get():
                handled = self.interface.HandleEvent(event)
                if not handled:
                    if event.type == QUIT:
                        self.done = True
                    elif event.type == KEYDOWN:
                        if event.key == K_ESCAPE:
                            self.done = True

                        ## Some sample reaction to events.
                        elif event.key == K_t:
                            ## This screen will end and go to another screen.
                            self.done = True
                            self.game_state.append("Title Screen")
                        elif event.key == K_p:
                            ## Return to _this_ screen after the pop-up.
                            self.done = True
                            self.game_state.append("Status Screen")
                            self.game_state.append("PopUp Screen")

            ## Draw a sample set of stuff on the screen.
            self.screen.fill((0,0,180,255))
            text = "This is the Status Screen. Esc= quit, T= Title Screen, P= Pop-Up Screen."
            text_rendered = self.font.render(text,1,(255,255,255))
            self.screen.blit(text_rendered,(100,100))
            status = self.font.render("# of game states on stack: "+str(len(self.game_state)),1,(255,255,255))
            self.screen.blit(status,(100,300))
            self.interface.Draw()

            pygame.display.update()
            self.clock.tick(20)

    def PopUpScreen(self):
        """A sample screen, resembling a pop-up dialog or something."""

        self.done = False
        while not self.done:

            ## Handle messages from elsewhere, such as the interface.
            for message in self.messages:
                headline = message.get("headline","")
                print "Got a message: "+headline

            self.messages = []

            ## Handle events.
            for event in pygame.event.get():
                handled = self.interface.HandleEvent(event)
                if not handled:
                    if event.type == KEYDOWN:
                        if event.key == K_ESCAPE:
                            ## This screen ends w/o putting another on stack.
                            self.done = True

            ## Draw a sample set of stuff on the screen.
            self.screen.fill((200,200,255),(50,50,700,500))
            text = "This is a Pop-Up Screen. Esc= leave this screen."
            text_rendered = self.font.render(text,1,(0,0,0))
            self.screen.blit(text_rendered,(100,100))
            status = self.font.render("# of game states on stack: "+str(len(self.game_state)),1,(0,0,0))
            self.screen.blit(status,(100,300))
            self.interface.Draw()

            pygame.display.update()
            self.clock.tick(20)

## Run the demo.
pygame.init()
g = Game()
g.Go()