Handling transitions between various screens was one of the problems my friends and I encountered when developing our shmup game at university. For instance, it was a pain getting the game flow moving from the title screen to the game screen, from game screen to loading screen, and so on.
Because of the way we initially structured the main loop, it was pretty awkward to shoehorn in these transitions. We ended up with this system where the title screen would come up, and part of its main loop would have a call to the game screen’s main loop. Effectively, we were two loops deep whenever we were running the game. It’s not exactly inefficient, but it’s not pretty. The answer I discovered during my internship, long after the project died, was relatively simple. Rather than treat these screens as different loops to jump between, I implemented a system which only uses one loop. This loop repeatedly calls the update function of the current handler. Handlers are the abstractions of each screen, containing their loop logic, assets and so forth. They derive from a common base class and override its update function. It’s quite a simple approach to arrive at, but when you’re just starting out you tend to prioritise the wrong things. Learning about this pattern in a professional environment was a definite boon. It was also probably covered at university (under the much more general polymorphism), but seeing the practical use cemented it in my mind. Later, I would use this approach when designing the platform game. It was so good, it was carried over to my current project.
Here’s the base class for handlers:
class Handler(object): # Handlers are the wrappers for the more separated parts of the game, # like the title screen, the main game screen, the game over screen.. def __init__(self, game): self.game = game self.running = True def update(self): print("Default handler") return True
And here’s a snippet from the title screen handler:
class TitleScreenHandler(Handler): ... def _draw(self): self.game.screen.blit(self.background, (0,0)) self._drawText() def _logic(self): for button in self.buttons: button.update() def _handleInput(self): self.inputHandler.update() def update(self): self._draw() self._logic() self._handleInput() return self.running
And of course, the part of the main loop responsible for calling the handlers in the first place:
def main(): ... while True: # Cap the frame rate. clock.tick(60) # Run the game handler. if not game.handler.update(): break # Show our hard work! pygame.display.flip()
As you may have noticed, the current handler is stored as part of a Game variable. The game class is meant to carry over cross-handler information. Normally we’d expect it to store some sort of game state, but most of the information is stored inside the handler. This Game object is passed in to the handler on creation, allowing a handler to transition to another by simply altering the current handler itself. Thanks to some behind-the-scenes Python magic, these self-orphaned handlers are cleaned up and not left hanging around.
Here’s an example of how we might transition (very quickly!) from the title screen handler to the game screen:
class TitleScreenHandler(Handler): ... def _startGame(self): self.game.handler = GameScreenHandler(self.game) ...
It’s pretty simple, right? We make sure the game state is passed on so further transitions can be performed. This system also allows us to do some clever things, such as allowing us to run handlers within handlers. For instance, if we wanted to create a loading handler such that the current handler continued running until some number of assets were ready, we could write something like:
class LoadingScreenHandler(Handler): ... def update(self): if (self.loading): self.currentHandler.update() else: self.game.handler = self.nextHandler ...
Here the loading handler runs its own handler before transferring control to a target handler after asset loading has finished (probably via a thread). Of course, this is getting ahead of ourselves: we have no assets! Indeed, we won’t for a good while yet. Rather, we now have the framework for implementing the many screens of the game. Next time, we’ll look at the title screen and input system in more detail.