Pong game using Pygame - step 3

By Martin McBride, 2022-10-14
Tags: game development pong
Categories: pygame


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

In the previous steps:

  • step 1, we created a window and displayed the bat and ball, with no animation.
  • step 2, we animated the ball, making it bounce off the 4 edges of the screen, but with no interaction with the bat.

In this step we will animate the bat, making it move in response to the mouse. We will also make the ball bounce off the bat. If the player misses the ball, the game will end.

This step is much shorter than the last two steps because we have already done a lot of the groundwork.

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

Code so far

This step only affects the behaviour of the bat and the ball. This means that main.py doesn't need to change at all. We won't copy the code here, refer to step 2 if you want to see it.

We will only need to make changes to sprites.py. Here is the sprite code we developed in step 2:

import pygame as pg

class Ball(pg.sprite.Sprite):

    def __init__(self, pos, game):
        super(Ball, self).__init__()
        self.game = game
        self.image = pg.image.load('ball.png')
        self.rect = self.image.get_rect()
        self.rect.center = pos
        self.speed = 0.2
        self.direction = pg.Vector2(1, -1)

    def update(self, delta_time):
        self.rect.center += self.direction * self.speed * delta_time
        if self.rect.colliderect(self.game.left):
            self.direction[0] = 1
        if self.rect.colliderect(self.game.right):
            self.direction[0] = -1
        if self.rect.colliderect(self.game.top):
            self.direction[1] = 1
        if self.rect.colliderect(self.game.bottom):
            self.direction[1] = -1


class Bat(pg.sprite.Sprite):

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

    def update(self, delta_time):
        pass

Moving the bat

We saw in step 2 how to move the ball (the code above). We did that by updating the ball's position in the update method of the Ball sprite. Here is how it worked:

  • In the initialisation in main.py we created a Ball sprite and added it to the all_sprites group.
  • In the game loop, also in main.py, we called all_sprites.update().
  • all_sprites.update() calls the update() method on every sprite in the group. This includes the Ball sprite.
  • The Ball sprite update() method updates the position of the ball and changes its direction if it collides with the edge of the game screen.

The existing code also adds the Bat sprite to the all_sprites group. This means that the update() method of the Bat sprite is also called on each pass through the game loop. But as you can see from the code above, the Bat sprite has an empty update() method so it does nothing.

We want the bat to follow the mouse in the horizontal direction. To do this we first need to find the mouse position. Pygame has a function to do that:

pg.mouse.get_pos()

This returns the (x, y) position of the mouse, in pixels, using the main window coordinates. Inside the Bat.update() method, we could do this:

def update(self, delta_time):
    self.rect.center = pg.mouse.get_pos()

The bat will now follow the mouse, left, right, up or down. Wherever the mouse goes, the centre of the bat will be right underneath it.

But this isn't quite what we want. We want the bat to follow the mouse in the x direction only. get_pos() returns an (x, y) tuple, so we can get just the x value of the position using:

pg.mouse.get_pos()[0]

We also need a y value for the bat position, which we can obtain from the current bat position self.rect.center[1] (taking just the y component of the position).

We combine these two parts to create a full (x, y) tuple of the required position of the bat:

def update(self, delta_time):
    self.rect.center = (pg.mouse.get_pos()[0], self.rect.center[1])

Here is the final code of the Bat sprite:

class Bat(pg.sprite.Sprite):

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

    def update(self, delta_time):
        self.rect.center = (pg.mouse.get_pos()[0], self.rect.center[1])

Updating the ball behaviour

We need to add two new behaviours to the Ball sprite:

  • Make it bounce when we hit the bat.
  • End the game when the player misses the ball.

The update method already performs checks to see if the ball has hit any edge of the screen, and bounce it in the opposite direction when it does. To bounce the ball off the bat, we just add one extra check in the ball update method:

if self.rect.colliderect(self.game.bat_sprite.rect):
    self.direction[1] = -1

Here we check if the ball (the self object) has collided with the bat sprite. Since the ball always approaches the bat in the downwards direction, when it hits we always want to make the ball travel upwards, which requires the ball y direction to be set to -1.

Next, we want to end the game when the player misses the ball. The easiest way to detect that is to detect when the ball hits the bottom of the screen. That can only happen when the player misses the ball.

The previous version of the game (step 2) already had a defined behaviour for the ball hitting the bottom of the screen. It would cause the ball to bounce upwards. In the ball update method we had:

if self.rect.colliderect(self.game.bottom):
    self.direction[1] = -1

Now instead of changing the ball direction, we simply need to set the game.running flag to false. If you recall from step 2, the main game loop is a while loop with game.running as its condition, so setting it false will terminate the game immediately. We don't need to change the direction of the ball because the game is ending anyway. So our new action is:

if self.rect.colliderect(self.game.bottom):
    self.game.running = False

The complete code for the ball and bat sprites is shown in the final code below.

The final code

Here is the final sprite.py code for this step:

import pygame as pg

class Ball(pg.sprite.Sprite):

    def __init__(self, pos, game):
        super(Ball, self).__init__()
        self.game = game
        self.image = pg.image.load('ball.png')
        self.rect = self.image.get_rect()
        self.rect.center = pos
        self.speed = 0.2
        self.direction = pg.Vector2(1, -1)

    def update(self, delta_time):
        self.rect.center += self.direction * self.speed * delta_time
        if self.rect.colliderect(self.game.left):
            self.direction[0] = 1
        if self.rect.colliderect(self.game.right):
            self.direction[0] = -1
        if self.rect.colliderect(self.game.top):
            self.direction[1] = 1
        if self.rect.colliderect(self.game.bottom):
            self.game.running = False
        if self.rect.colliderect(self.game.bat_sprite.rect):
            self.direction[1] = -1


class Bat(pg.sprite.Sprite):

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

    def update(self, delta_time):
        self.rect.center = (pg.mouse.get_pos()[0], self.rect.center[1])

Summary

So, in this section we have:

  • Modified the Bat.update() method to cause the bat to move in response to the mouse.
  • Modified the Ball.update() method to cause the ball to bounce off the bat.
  • Modified the Ball.update() method to make the game end if the player misses the ball.

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