Pong game using Pygame - step 1

By Martin McBride, 2022-09-02
Tags: game development pong
Categories: pygame


This is the first article in a series where we will develop a complete game of Pong using Pygame.

In this step, we will simply create a window and display the bat and ball, with no animation.

The code for this article can be found on github, and the resources are here.

Creating a basic game window

Here is the code to create a simple Pygame window:

import pygame as pg

# Setup game
screen_width = 640
screen_height = 480

# Initialise pygame
pg.init()

# Set display size
pg.display.set_mode((screen_width, screen_height))

# Set window title
pg.display.set_caption('Pong')

# Game loop

running = True

while running:

    # Check all events in queue
    for event in pg.event.get():

        # If a quit event occurs, reset running flag
        if event.type == pg.QUIT:
            running = False

This code doesn't do a great deal - it just displays a blank window - but it is the basic set-up any game will need. We will look at it step by step.

The first thing we need to do is import the pygame module. We import it as pg, which means that in our code we can refer to it as pg. You don't have to do this, but it makes your code slightly more readable:

import pygame as pg

Next, we initialise Pygame, like this:

# Setup game
screen_width = 640
screen_height = 480

# Initialise pygame
pg.init()

# Set display size
pg.display.set_mode((screen_width, screen_height))

# Set window title
pg.display.set_caption('Pong')

There are three steps:

  • pg.init() initialises the library.
  • pg.display.set_mode() sets the screen size to 640 by 480. You can change this if you wish, for example to 800 by 600
  • pg.set_caption() sets the title bar of the window.

Here is the result:

Empty game window created with Pygame

Sprites

What is a sprite? In game development, a sprite is a 2-dimensional graphical element of a game. It can be a player, a ball, a monster, a bullet, a spaceship, an asteroid, and so on. It can be an active element or it can be a passive element such as a wall, floor, obstacle, etc.

A sprite usually has an image and often has associated behaviours and interactions.

In our game, we have two sprites: a bat and a ball. The bat moves left and right under the control of the user's mouse. The ball moves around automatically and will bounce off the bat or the boundaries of the screen.

In Pygame, a sprite is created as a Python class. The class is based on the Pygame Sprite class, but each sprite we define can add its own behaviours.

We will implement our sprites in a separate Python file called sprites.py, It is easier to manage a program if you split it into different files each containing a set of related classes or functions. Here is the code for sprites.py:

import pygame as pg

class Ball(pg.sprite.Sprite):

    def __init__(self, pos):
        super(Ball, self).__init__()
        self.image = pg.image.load('ball.png')
        self.rect = self.image.get_rect()
        self.rect.center = pos

    def update(self):
        pass

class Bat(pg.sprite.Sprite):

    def __init__(self, pos):
        super(Bat, self).__init__()
        self.image = pg.image.load('bat.png')
        self.rect = self.image.get_rect()
        self.rect.center = pos

    def update(self):
        pass

Classes and objects

You will probably be familiar with Python objects. For example:

s1 = "abcd"
s2 = "xyz"
print(type(s1)) # <class 'str'>
print(type(s2)) # <class 'str'>
print(s1.upper()) # "ABCD"
print(s2.upper()) # "XYZ"

In this code, s1 and s2 are both strings. When we print the type of s1 we get:

<class 'str'>

This tells us that s1 has class str, in other words, it has type str. Same for s2.

Both values are the same type, but of course, they contain different data. s1 contains the text "abcd", but s2 contains the text "xyz".

s1 and s2 are different objects, but they are both the same type (or class) of object.

The string class has several functions associated with it. A function belonging to a class is usually called a method but it is more or less the same thing.

For example, strings have a method upper that returns a copy of the text, converted to uppercase. So s1.upper() gives "ABCD" and s2.upper() gives "XYZ".

To summarise:

  • A class defines a type of object. It defines what sort of values the object can hold, and also defines methods that can be called pt act on that data.
  • An object is an instance of a class. For example s1 is an instance of the string class (put more simply, it is a string). An object has its own unique data, but it also has the methods defined by its class.

Objects are a great way of combining data, together with useful functions to operate on that data, into a single convenient item. We will use them quite a lot in game development, and often define our own classes.

Defining the sprites

The Ball class is defined like this:

class Ball(pg.sprite.Sprite):

    def __init__(self, pos):
        super(Ball, self).__init__()
        self.image = pg.image.load('ball.png')
        self.rect = self.image.get_rect()
        self.rect.center = pos

    def update(self):
        pass

This code defines a class called Ball that represents a ball sprite in our game. We give our sprite two methods:

  • __init__ is a special method that is used to initialise an object when it is created.
  • update is called once on each pass through the game loop to update the sprite.

One other thing about this class - it inherits from the Sprite class (because we pass sprite.Sprite into the Ball class declaration). This means that our class will already have some useful behaviours without us having to write any code at all.

The __init__ method is where we set up new Ball objects. We do several things in this method:

  • Call super.__init__(). This initialises the parent Sprite class. This is very important, if we don't do this the sprite behaviours won't be initialised properly and might cause weird errors or bugs.
  • Load the ball.png image and store it in self.image.
  • Find the size of the image using self.image.get_rect(), and use that to set the size of the sprite self.rect.
  • Take the initial position pos (passed in when the ball is constructed) and use it to set the position of the centre of the sprite.

Methods within a class have a special extra parameter self that represents the object itself. So when we declare self.image it creates a variable that belongs to the object (called a member variable) that can be accessed in other methods (again as self.image).

Looking back to the original sprite.py file, you will see that we also define a Bat class that is currently almost identical to the Ball class, except that it loads a bat.png as its image.

Creating the bat and ball

Now we will change the main game code to create and use the sprites. Here is the new code:

import pygame as pg
from sprites import Ball, Bat

# Setup game

screen_width = 640
screen_height = 480

# Initialise pygame
pg.init()

# Set display size
screen = pg.display.set_mode((screen_width, screen_height))

# Set window title
pg.display.set_caption('Pong')

# Create sprites

ball_sprite = Ball((100, 200))
bat_sprite = Bat((200, 400))
all_sprites = pg.sprite.RenderPlain()
all_sprites.add(ball_sprite)
all_sprites.add(bat_sprite)

# Game loop

running = True

while running:

    # Check all events in queue
    for event in pg.event.get():

        # If a quit event occurs, reset running flag
        if event.type == pg.QUIT:
            running = False

    screen.fill((0, 0, 0))
    all_sprites.draw(screen)
    pg.display.flip()

This is very similar to the previous code that displays a blank screen, we have just added 2 sections to create the sprites, and to display them.

Here is the window it creates. The bat and ball are stationary and will never move:

Pong game window created with Pygame with fixed bat and ball sprites

Here is the creation code:

ball_sprite = Ball((100, 200))
bat_sprite = Bat((200, 400))
all_sprites = pg.sprite.RenderPlain()
all_sprites.add(ball_sprite)
all_sprites.add(bat_sprite)

This code creates a ball_sprite by calling the Ball function. It passes in the parameter (100, 200), a tuple that sets the centre of the ball to a position that is 100 pixels in from the left of the game window, and 200 pixels down from the top.

We create a bat_sprite in a similar way, at position (200, 400).

We then add both sprites to a sprite group.

Sprite groups

A sprite group is an object that holds a collection of sprites. Sprite groups are very useful because they allow us to operate on all the sprites in a group by making a single call.

In our case, we have created a sprite group called all_sprites that holds the ball and bat sprites. The group is a RenderPlain object, which is a special type of sprite group that can automatically draw all the sprites.

Having created the group, we then add the ball and bat sprites using the add method.

Drawing the sprites

To draw the sprites, we just need to add this code to the game loop:

    screen.fill((0, 0, 0))
    all_sprites.draw(screen)
    pg.display.flip()

This code is called each time the game loop runs, which is typically many times a second.

We draw using the screen object we created earlier. Here is what the code does:

  • First we clear the screen to black by filling it with the RGB colour value (0, 0, 0).
  • Next we draw the sprites to the screen using the all_sprites.draw method. Since all_sprites contains our ball and bat, both those items will be drawn.
  • Finally, we call display.flip() to make display the screen, as described below.

Double buffering

Why do we need to call the flip() method to display the sprites?

The reason is that the screen object is not the actual screen that the user sees. screen is a hidden memory buffer that stores the pixels' colours but doesn't display them.

There is a good reason for this. In most games, the sprites are moving around. We need to first remove the old sprites (by filling the screen with black pixels) and then redraw the sprites in their new position.

If we did this on the real screen, the whole screen would flicker each time through the game loop, It would look terrible, and probably give the players a headache.

To avoid this, Pygame, like most game engines, uses double buffering. The screen object is a hidden memory buffer. We can clear it and redraw it without having any visible effect on the computer screen.

When we have completely finished all our drawing operations, we call the display.flip() method that quickly copies the memory from the hidden buffer to the real screen memory.

File structure

Since this is a simple project, all our files live in the same folder. It contains:

  • The main module main.py.
  • The sprites module sprites.py
  • The ball image ball.png.
  • The bat image bat.png.

Summary

We have covered quite a lot of ground here. It might seem like a lot of trouble, just to display a fixed, unmoving image of a ball and bat.

But in fact, we have done more than that. We have laid the groundwork for the next steps of our game, which will come together quite quickly in the next few stages.

See also

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest