Discover Python and Patterns (11): Class

Published September 28, 2020
Advertisement

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

Create a class with Python

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.

Constructor and attributes

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.

Method call

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!).

Game Loop pattern

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.

The __init__() method

The __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

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.

The update() method

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

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

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.

Create and run the game

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.

Complete program

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.

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement
Advertisement