The implementation of the Game Loop pattern in the previous post is not good: I propose to use the Command pattern to get a better one. This pattern allows separating input handling and game updating.
This post is part of the Discover Python and Patterns series
The main idea behind this pattern is to separate the origin of the modifications from their implementation. For instance, when the player presses an arrow key, the controlled character is not moved. We first memorize that the player wants to move the character in a direction. Then, in a second time, the character is moved according to what we memorized.
It is quite a natural pattern. For instance, when you command a meal in a restaurant, the waiter notes your order on a piece of paper. Then, he goes to the kitchen and gives it to the cooker. The cooker is free to prepare the meals in the best order. For instance, if several customers are asking for the same meal, he can group the preparations.
In software design, this is very similar. The customer is the user (or the player in games) asking for the execution of a task. These orders, or commands, are stored in variables. Then, the code lines that execute tasks read the commands and act accordingly.
As for all patterns, there are many ways to implement it. For our game example, I'll use class attributes to store the commands. We could also use more advanced representation as a class for each kind of command.
Let's use the Command pattern to get a better Game Loop pattern.
I first create two new attributes in the constructor to store the move direction the player is asking for:
def __init__(self): ... the first lines are as before ... self.moveCommandX = 0 self.moveCommandY = 0
Then, in the
processInput() method, I initialize these two attributes with a zero value: it means that, by default, there is no movement. In the event processing loop, and more specifically in the processing of arrow keys, I no more update the
y attributes (the location of my rectangle = my game state). I now store the shift I want to apply to this location:
def processInput(self): self.moveCommandX = 0 self.moveCommandY = 0 for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False break elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.running = False break elif event.key == pygame.K_RIGHT: self.moveCommandX = 8 elif event.key == pygame.K_LEFT: self.moveCommandX = -8 elif event.key == pygame.K_DOWN: self.moveCommandY = 8 elif event.key == pygame.K_UP: self.moveCommandY = -8
update() method now has a content: it updates the location of the rectangle (
y attributes) according to the values of the move command attributes:
def update(self): self.x += self.moveCommandX self.y += self.moveCommandY
If you run the program with these changes, it works as before. So you could ask yourself: why should I do this? I was working fine before!
We are following a golden rule in software design: divide and conquer. It is impossible to solve complex problems directly, even for the most clever developer. There is always a level of complexity that becomes intractable. The solution is then to split a complex problem into simpler problems. If these sub-problems are still too complex, we split again. And we repeat, until getting problems simple enough to be solved.
It is what the Game Loop pattern proposes to do for the very complex problem of game development. It splits it into three main sub-problems: input handling, game update, and rendering. For this to work, the splitting must be effective.
In the previous implementation, we didn't separate input handling and game updating. Sooner or later, we would have faced too complex problems. Thanks to the improvement using the Command pattern, we are better protected from these future issues.
If you are still not convinced, let's consider the case of controls. In our game example, we use the keyboard arrows. But what about using a gamepad, for instance? With the first implementation, we would have to duplicate the update of the
y attributes for each control. In our example, this is very simple, but it is easy to imagine more complex cases where we have to deal with collisions and so on. With the new implementation, we only have to create commands for each control (an easy task), and only need a single implementation of the game updating (a more complex task).
Now that I've introduced the "divide and conquer" golden rule, we'll try to use it as much as possible. For instance, in our current implementation of the game, the game state and the user interface (=input handling and rendering) are mixed.
I propose to create a new class
GameState that handles the game state (storage and update):
class GameState(): def __init__(self): self.x = 120 self.y = 120 def update(self,moveCommandX,moveCommandY): self.x += moveCommandX self.y += moveCommandY
__init__() constructor, I create the
update() method, I update the coordinates using the move command values.
In the constructor of my main class, I replace the creation of the
def __init__(self): ... self.x = 120 self.y = 120 ...
with the creation of a new
def __init__(self): ... self.gameState = GameState() ...
update() method, I call the
update() method of the
def update(self): self.gameState.update(self.moveCommandX,self.moveCommandY)
And finally, in the
render() method, I use the attributes in the game state instead of the ones in the main class. For instance,
That way, all problems related to game data storage and updating is delegated to the
GameState class. The main class no more has to worry about that, and it only has to know the name of the attributes and methods it can use.
import os import pygame os.environ['SDL_VIDEO_CENTERED'] = '1' class GameState(): def __init__(self): self.x = 120 self.y = 120 def update(self,moveCommandX,moveCommandY): self.x += moveCommandX self.y += moveCommandY class UserInterface(): def __init__(self): pygame.init() self.window = pygame.display.set_mode((640,480)) pygame.display.set_caption("Discover Python & Patterns - https://www.patternsgameprog.com") pygame.display.set_icon(pygame.image.load("icon.png")) self.clock = pygame.time.Clock() self.gameState = GameState() self.running = True self.moveCommandX = 0 self.moveCommandY = 0 def processInput(self): self.moveCommandX = 0 self.moveCommandY = 0 for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False break elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.running = False break elif event.key == pygame.K_RIGHT: self.moveCommandX = 8 elif event.key == pygame.K_LEFT: self.moveCommandX = -8 elif event.key == pygame.K_DOWN: self.moveCommandY = 8 elif event.key == pygame.K_UP: self.moveCommandY = -8 def update(self): self.gameState.update(self.moveCommandX,self.moveCommandY) def render(self): self.window.fill((0,0,0)) x = self.gameState.x y = self.gameState.y pygame.draw.rect(self.window,(0,0,255),(x,y,400,240)) pygame.display.update() def run(self): while self.running: self.processInput() self.update() self.render() self.clock.tick(60) userInterface = UserInterface() userInterface.run() pygame.quit()
In the next post, we'll start to use sprites to render our game!