Nerd Paradise
Home
About
Puzzles
Math
Programming
Origami
Japanese
MSPaint
Home > Programming > PyGame Tutorial > PyGame Tutorial: Centralized Scene Logic

PyGame Tutorial: Centralized Scene Logic

This isn't a PyGame-specific tutorial per-se. It's more of an application of good software design concepts. This model of doing things has served me well for many complicated games.

If you are not familiar with Object-Oriented programming in Python, familiarize yourself now.

Done? Excellent.

Here is a class definition for a SceneBase:
class SceneBase:
    def __init__(self):
        self.next = self
    
    def ProcessInput(self, events):
        print("uh-oh, you didn't override this in the child class")

    def Update(self):
        print("uh-oh, you didn't override this in the child class")

    def Render(self, screen):
        print("uh-oh, you didn't override this in the child class")

    def SwitchToScene(self, next_scene):
        self.next = next_scene


When you override this class, you have 3 method implementations to fill in.
  • ProcessInput - This method will receive all the events that happened since the last frame.
  • Update - Put your game logic in here for the scene.
  • Render - Put your render code here. It will receive the main screen Surface as input.

Of course, this class needs the appropriate harness to work. Here is an example program that does something simple: It launches the PyGame pipeline with a scene that is a blank red background. When you press the ENTER key, it changes to blue. This code may seem like overkill, but it does lots of other subtle things as well while at the same time keeps the complexity of your game logic contained into a snazzy OO model. Once you start adding more complexity to your game, this model will save you lots of headaches. Additional benefits are listed below.
# The first half is just boiler-plate stuff...

import pygame

class SceneBase:
    def __init__(self):
        self.next = self
    
    def ProcessInput(self, events, pressed_keys):
        print("uh-oh, you didn't override this in the child class")

    def Update(self):
        print("uh-oh, you didn't override this in the child class")

    def Render(self, screen):
        print("uh-oh, you didn't override this in the child class")

    def SwitchToScene(self, next_scene):
        self.next = next_scene
    
    def Terminate(self):
        self.SwitchToScene(None)

def run_game(width, height, fps, starting_scene):
    pygame.init()
    screen = pygame.display.set_mode((width, height))
    clock = pygame.time.Clock()

    active_scene = starting_scene

    while active_scene != None:
        pressed_keys = pygame.key.get_pressed()
        
        # Event filtering
        filtered_events = []
        for event in pygame.event.get():
            quit_attempt = False
            if event.type == pygame.QUIT:
                quit_attempt = True
            elif event.type == pygame.KEYDOWN:
                alt_pressed = pressed_keys[pygame.K_LALT] or \
                              pressed_keys[pygame.K_RALT]
                if event.key == pygame.K_ESCAPE:
                    quit_attempt = True
                elif event.key == pygame.K_F4 and alt_pressed:
                    quit_attempt = True
            
            if quit_attempt:
                active_scene.Terminate()
            else:
                filtered_events.append(event)
        
        active_scene.ProcessInput(filtered_events, pressed_keys)
        active_scene.Update()
        active_scene.Render(screen)
        
        active_scene = active_scene.next
        
        pygame.display.flip()
        clock.tick(fps)

# The rest is code where you implement your game using the Scenes model

class TitleScene(SceneBase):
    def __init__(self):
        SceneBase.__init__(self)
    
    def ProcessInput(self, events, pressed_keys):
        for event in events:
            if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
                # Move to the next scene when the user pressed Enter
                self.SwitchToScene(GameScene())
    
    def Update(self):
        pass
    
    def Render(self, screen):
        # For the sake of brevity, the title scene is a blank red screen
        screen.fill((25500))

class GameScene(SceneBase):
    def __init__(self):
        SceneBase.__init__(self)
    
    def ProcessInput(self, events, pressed_keys):
        pass
        
    def Update(self):
        pass
    
    def Render(self, screen):
        # The game scene is just a blank blue screen
        screen.fill((00255))

run_game(40030060, TitleScene())


Other awesome things you can easily do with this:
  • You can change the screen mode with a hotkey. With the event filtering, you can add another clause to check for something like 'f' or F11 and then re-initialize the display to fullscreen or something. (if you pass pygame.FULLSCREEN in as a 2nd argument to pygame.display.set_mode, it will create a fullscreen window)
  • Another huge advantage is you can create your own input model. Instead of simply filtering out pygame events, you can map the pygame events to your own event class. Instead of checking for pygame.K_SPACE to see if Hero Dude should fire his lazor, you can create a custom event where your check looks something more like myevent.type == 'FIRE_LAZOR'. The beauty of this is you can write an input configuration menu where you can map keys to actions or offer presets for various keyboard types (such as Dvorak users who get angry at programmers who use w/a/s/d keys for movement). Putting this logic in one centralized location keeps you from having to worry about all this each time you need to check the keys (just be sure to modify get_pressed accordingly if you do this).
  • Ditto ^ for JoyStick/Gamepad functionality.
  • Currently, the user cannot resize the window. In a traditional code layout, you'd have to rewrite all your render code to take into consideration the scale of the resized window. However, you can modify the above code to create an intermediate screen Surface object that you pass in to the scenes' Render method. This intermediate screen will be the size of the logical width and height of the game (in this case, 400 x 300). Then, for each frame, you can use PyGame's scale transforms to adjust the logical screen to the size of the real window. This way, your code can pretend that the size of the window is always 400 x 300, but the actual size of the window is unconstrained.

Remember, clean code is happy code!

You are visitor #002486
Get notified about new posts by following the newfangled twitter account.
Best viewed with
© 1999 Nerd Paradise