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
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((255, 0, 0))
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((0, 0, 255))
run_game(400, 300, 60, TitleScene())
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((255, 0, 0))
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((0, 0, 255))
run_game(400, 300, 60, 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!