I propose to introduce Python classes to implement the Game Loop pattern. Using these tools, I show how to refactor the code in the previous post to get a more robust and readable program.
This post is part of the Discover Python and Patterns series
In this post, I introduce the basics of classes in Python. I can't present everything if I don't want to lose you, so this is not exhaustive. There are also some approximations I'll correct in the following posts.
In Python, you can create a class using the following syntax:
class MyClass():
def __init__(self,value):
self.value = value
def incValue(self):
self.value += 1
The name of this class is MyClass
. You can choose any available name as usual. Note that a lot of programmers start class names with capital letters.
This class has two methods: __init__()
and incValue()
. Methods are like functions, except that they are defined in a class.
The first argument of a method is always a reference to a class instance, and most Python programmers name it self
. A class instance is an object of a class: it contains all the attributes and methods of the class.
The body of methods is like that of functions: the following code block defined by the indentation contains all the lines of the method.
The method __init__()
is a special method called the constructor. Python calls it when you create a new class instance. As we saw before, we can create a class instance by using the class name as a function:
myClass = MyClass(10)
Note that there is only one argument to this call: it is as if we are ignoring the self
argument of the __init__()
method (or constructor).
Inside the __init__()
method, we create an attribute value
using the dot operator:
def __init__(self,value):
self.value = value
The expression self.value = value
creates the attribute with an initial value copied from value
. Note that, since Python is a dynamic language, you can create an attribute in any method of a class, and also outside the class. However, I don't recommend it! Please code as if it was impossible until you get a strong knowledge of Python. Only create attributes in the constructor.
Once an attribute exists, you can access it easily using the dot operator:
myClass = MyClass(10)
print(myClass.value)
This program displays "10" in the console.
To call a class method, you need a reference to a class instance, and then use the dot operator:
myClass = MyClass(10)
myClass.incValue()
print(myClass.value)
This program displays "11" in the console.
Note that, as for the constructor, we ignore the first argument self
. The class instance before the dot defines this value. There is a function call equivalent of a method call, for the method incValue()
it is:
MyClass.incValue(myClass)
We can see the use of the first argument that sets self
to myClass
. A method call is finally a syntax improvement that eases coding and reading (as well as other nice properties, but this is another story!).
Using a class, we can get a better implementation of the Game Loop pattern:
class Game():
def __init__(self):
... Initialization ...
def processInput(self):
... Handle user input ...
def update(self):
... Update game state ...
def render(self):
... Render game state ...
def run(self):
... Main loop ...
The class contains all the methods related to the pattern. Furthermore, we no more need to add many arguments to these methods since all data is in the class attributes.
Let's now see the implementation of all these methods, which use the same code as in the previous post, but organized differently.
__init__()
methodThe __init__()
method contains all the code at the beginning of the previous program, except that data is put in the class attributes:
def __init__(self):
pygame.init()
self.window = pygame.display.set_mode((640,480))
pygame.display.set_caption("Discover Python & Patterns")
pygame.display.set_icon(pygame.image.load("icon.png"))
self.clock = pygame.time.Clock()
self.x = 120
self.y = 120
self.running = True
For instance, the window handle is in the window
attribute thanks to the expression self.window = pygame.display.set_mode((640,480))
.
It means that, if we type self.window
in any method of this class, then we get the value of the window
attribute.
The processInput()
method contains the for
loop that processes the Pygame events:
def processInput(self):
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.x += 8
elif event.key == pygame.K_LEFT:
self.x -= 8
elif event.key == pygame.K_DOWN:
self.y += 8
elif event.key == pygame.K_UP:
self.y -= 8
It is as before, except that we set or update class attributes. For instance, x += 8
becomes self.x += 8
.
In the Game Loop pattern, the update()
method should change the game state. In our case, this data is made of the x
and y
attributes. However, we already update them in the processInput()
. As a result, there is nothing to do during the update. In Python, to tell that there is nothing to do, you can use the pass
keyword:
def update(self):
pass
This implementation of the Game Loop pattern is not good. I didn't change the previous code, except for the use of the class attributes. It would have been too complex to do everything at the same time. I'll show in the next post how to get a better implementation of the Game Loop pattern.
The render()
method uses the data from the game state (x
and y
attributes) to display the current state of the game:
def render(self):
self.window.fill((0,0,0))
pygame.draw.rect(self.window,(0,0,255),(self.x,self.y,400,240))
pygame.display.update()
As in the previous methods, I just replaced the global variables with attributes, like window
that becomes self.window
.
The run()
method contains the main loop, and calls the other methods:
def run(self):
while self.running:
self.processInput()
self.update()
self.render()
self.clock.tick(60)
We call a method with the dot operator. For instance, self.update()
calls the method update()
using the class instance referenced by self
.
Each game step (input, update, and render) is run 60 times per second (line self.clock.tick(60)
). It is the simplest case, but we could change that in this method. For instance, we could only render half of the frames on slow computers. That way, the game still runs the same on every computer (slow and fast ones), and we don't have to change anything in the input processing and game state update. It also means that, if we want to run the game on a server with no display, we only need to remove the call to the render()
method.
Finally, to run and close the game, we only need the following lines:
game = Game()
game.run()
pygame.quit()
Line 1 creates a new instance of the Game
class. It is the Game
class used as if it was a function. The __init__()
method has only one argument (self
), so there are no arguments in this call.
Line 2 calls the run()
method of the game
instance.
Line 3 calls the quit()
function of the pygame
package to close all Pygame content.
import os
import pygame
os.environ['SDL_VIDEO_CENTERED'] = '1'
class Game():
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.x = 120
self.y = 120
self.running = True
def processInput(self):
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.x += 8
elif event.key == pygame.K_LEFT:
self.x -= 8
elif event.key == pygame.K_DOWN:
self.y += 8
elif event.key == pygame.K_UP:
self.y -= 8
def update(self):
pass
def render(self):
self.window.fill((0,0,0))
pygame.draw.rect(self.window,(0,0,255),(self.x,self.y,400,240))
pygame.display.update()
def run(self):
while self.running:
self.processInput()
self.update()
self.render()
self.clock.tick(60)
game = Game()
game.run()
In the next post, I'll show you a better implementation of the Game Loop pattern using the Command pattern.