7
\$\begingroup\$

I wrote a very simple Ping Pong game using pygame and everything worked out quite well. The only issue is the high CPU usage, which seems a bit strange. One thread is immediately occupied up to 100% after starting the application.

I am currently using a pygame.clock and clock.tick(30) inside my main loop. Therefore, pygame shouldn't occupied as many CPU cycles as possible. Additionally, I replaced the use of pygame.event.get() (sometimes mentioned as a potential bottleneck), which did not change the CPU usage.

main.py

import pygame
from colors import *
from ball import Ball
from racket import Racket
from directions import Directions
from player import Player

clock = pygame.time.Clock()

WIN_WIDTH = 800
WIN_HEIGHT = 640
MAX_SCORE = 5
DISPLAY = (WIN_WIDTH, WIN_HEIGHT)
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode(DISPLAY, 0, 32)

DONE = False
FPS = 30

left_player = Player(Directions.LEFT, 'Left')
right_player = Player(Directions.RIGHT, 'Right')

curr_ball = Ball(screen, WIN_WIDTH, WIN_HEIGHT)

left_racket = Racket(screen, WIN_WIDTH, WIN_HEIGHT, Directions.LEFT)
right_racket = Racket(screen, WIN_WIDTH, WIN_HEIGHT, Directions.RIGHT)

rackets = pygame.sprite.Group()
rackets.add(left_racket)
rackets.add(right_racket)
stuff_to_draw = pygame.sprite.Group()
stuff_to_draw.add(left_racket)
stuff_to_draw.add(right_racket)


def game_over(screen, winner, left_paper, right_player):
    gray_overlay = pygame.Surface((WIN_WIDTH, WIN_HEIGHT))
    gray_overlay.fill(GRAY)
    gray_overlay.set_colorkey(GRAY)
    pygame.draw.rect(gray_overlay, BLACK, [0, 0, WIN_WIDTH, WIN_HEIGHT])
    gray_overlay.set_alpha(99)
    screen.blit(gray_overlay, (0, 0))
    font = pygame.font.SysFont(None, 100)
    game_over = font.render('{} Player WINS!'.format(winner.name), True, WHITE)
    screen.blit(game_over, (WIN_WIDTH / 2 - 300, WIN_HEIGHT / 2 - 100))
    scoreline = font.render(
        '{} - {}'.format(left_paper.score, right_player.score), True, WHITE)
    screen.blit(scoreline, (WIN_WIDTH / 2 - 50, WIN_HEIGHT / 2 + 100))
    pygame.display.update()
    pygame.time.delay(2000)

while not DONE:
    screen.fill(BLACK)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            DONE = True

    pygame.event.pump()
    keys = pygame.key.get_pressed()
    if keys[pygame.K_q]:
        DONE = True
    if keys[pygame.K_UP]:
        right_racket.move_up()
    if keys[pygame.K_DOWN]:
        right_racket.move_down()
    if keys[pygame.K_w]:
        left_racket.move_up()
    if keys[pygame.K_s]:
        left_racket.move_down()

    stuff_to_draw.update()
    curr_ball.update()

    col_left, col_right = curr_ball.rect.colliderect(left_racket.rect), curr_ball.rect.colliderect(right_racket.rect)
    if col_right == 1 or col_left == 1:
        curr_ball.toggle_direction()
        curr_ball.hit()

    if curr_ball.get_x_val() <= 0:  # left border
        right_player.score = 1
        curr_ball = Ball(screen, WIN_WIDTH, WIN_HEIGHT)
    elif curr_ball.get_x_val() >= WIN_WIDTH:  # right border
        left_player.score = 1
        curr_ball = Ball(screen, WIN_WIDTH, WIN_HEIGHT)

    # Print scores
    font = pygame.font.SysFont('Helvetica', 25)

    left_player_score = font.render(
        '{}'.format(left_player.score), True, (255, 255, 255))
    right_player_score = font.render(
        '{}'.format(right_player.score), True, (255, 255, 255))
    goal_text = font.render(
        '{}'.format(MAX_SCORE), True, (255, 255, 0))

    screen.blit(left_player_score, (WIN_WIDTH / 2 - 100, 10))
    screen.blit(right_player_score, (WIN_WIDTH / 2 + 100, 10))
    screen.blit(goal_text, (WIN_WIDTH / 2, 0))

    stuff_to_draw.draw(screen)
    curr_ball.draw(screen)

    if left_player.score >= MAX_SCORE:
        game_over(screen, left_player, left_player, right_player)
    elif right_player.score >= MAX_SCORE:
        game_over(screen, right_player, left_player, right_player)

    if left_player.score >= MAX_SCORE or right_player.score >= MAX_SCORE:
        DONE = True

    pygame.display.set_caption('Ping Pong '+ str(clock.get_fps()))

    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()

colors.py

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GRAY = (64, 64, 64)

ball.py

import pygame
import random
from colors import *
from directions import *


class Ball(pygame.sprite.Sprite):

    def __init__(self, screen, width, height):
        super().__init__()

        self.width, self.height = width, height
        self.direction = random.choice([Directions.DOWN_LEFT, Directions.DOWN_RIGHT, Directions.UP_LEFT, Directions.UP_RIGHT])
        self.screen = screen
        self.image = pygame.Surface([10, 10])
        self.image.fill(WHITE)
        pygame.draw.rect(self.image, WHITE, [0, 0, 10, 10])
        self.rect = self.image.get_rect()
        self.position = (width / 2 + 2, height / + 2)
        self.hits = 0
        self.speed_up = 1.0

    def draw(self, screen):
        screen.blit(self.image, self.rect)

    def hit(self):
        self.hits += 1
        self.speed_up = 1.0+self.hits/10

    @property
    def position(self):
        return (self.rect.x, self.rect.y)

    @position.setter
    def position(self, pos):
        try:
            pos_x, pos_y = pos
        except ValueError:
            raise ValueError("Pass an iterable with two items")
        else:
            self.rect.x, self.rect.y = pos_x, pos_y

    def up_left(self):
        self.position = (self.position[0] - 10*self.speed_up, self.position[1] - 10*self.speed_up)

    def up_right(self):

        self.position = (self.position[0] + 10*self.speed_up, self.position[1] - 10*self.speed_up)

    def down_left(self):

        self.position = (self.position[0] - 10*self.speed_up, self.position[1] + 10*self.speed_up)

    def down_right(self):

        self.position = (self.position[0] + 10*self.speed_up, self.position[1] + 10*self.speed_up)

    def update(self):
        if self.position[1] <= 10:  # upper border
            self.direction = random.choice(
                [Directions.DOWN_LEFT, Directions.DOWN_RIGHT])
        if self.position[1] >= self.height - 10:  # bottom border
            self.direction = random.choice(
                [Directions.UP_LEFT, Directions.UP_RIGHT])

        options = {Directions.UP_LEFT: self.up_left,
                   Directions.UP_RIGHT: self.up_right,
                   Directions.DOWN_LEFT: self.down_left,
                   Directions.DOWN_RIGHT: self.down_right,
                   }
        options[self.direction]()

    def toggle_direction(self):
        if self.direction == Directions.DOWN_LEFT:
            new_direction = Directions.DOWN_RIGHT

        if self.direction == Directions.DOWN_RIGHT:
            new_direction = Directions.DOWN_LEFT

        if self.direction == Directions.UP_RIGHT:
            new_direction = Directions.UP_LEFT

        if self.direction == Directions.UP_LEFT:
            new_direction = Directions.UP_RIGHT

        try:
            self.direction = new_direction
        except NameError:
            pass

    def get_x_val(self):
        return self.rect.x

racket.py

import pygame
from colors import *
from directions import *


class Racket(pygame.sprite.Sprite):

    def __init__(self, screen, width, height, side):
        super().__init__()

        self.width, self.height = width, height
        self.racket_height = 100
        self.movement_speed = 20
        offset = 20
        self.screen = screen
        self.image = pygame.Surface([10, self.racket_height])
        self.image.fill(WHITE)
        pygame.draw.rect(self.image, WHITE, [0, 0, 10, self.racket_height])
        self.rect = self.image.get_rect()
        print(side)
        if side is Directions.LEFT:
            self.position = (offset, self.height / 2)
        else:
            self.position = (self.width - offset - 10, self.height / 2)

    @property
    def position(self):
        return (self.rect.x, self.rect.y)

    @position.setter
    def position(self, pos):
        try:
            pos_x, pos_y = pos
        except ValueError:
            raise ValueError("Pass an iterable with two items")
        else:
            self.rect.x, self.rect.y = pos_x, pos_y

    def move_up(self):
        if self.position[1] > 0:
            self.position = (self.position[0], self.position[1] - self.movement_speed)

    def move_down(self):
        if self.position[1] + self.racket_height < self.height:
            self.position = (self.position[0], self.position[1] + self.movement_speed)

directions.py

from enum import Enum


class Directions(Enum):
    UP_LEFT = 7
    UP_RIGHT = 9
    DOWN_LEFT = 1
    DOWN_RIGHT = 3
    LEFT = 4
    RIGHT = 6

player.py

class Player():

    def __init__(self, side, name):
        self.side = side
        self.points = 0
        self.name = name

    @property
    def score(self):
        return self.points

    @score.setter
    def score(self, val):
        self.points += val
\$\endgroup\$
  • \$\begingroup\$ Please post the complete project, otherwise we can't run and profile the game. \$\endgroup\$ – skrx Aug 30 '17 at 23:21
  • \$\begingroup\$ Sorry. Now all the files are included. \$\endgroup\$ – Tauling Aug 31 '17 at 10:28
  • \$\begingroup\$ I've just run and profiled the program and the performance doesn't seem to be bad. Is your hardware relatively old? Pygame uses only software rendering, so it's rather slow, but a simple game as Pong should run without problems. The only thing that you shouldn't do is, instantiating a new font object each frame (do it once before the while loop). \$\endgroup\$ – skrx Aug 31 '17 at 13:04
  • \$\begingroup\$ Give FPS a small value, like 2. Verify you have idle CPU cycles. Keep bumping it up until you saturate a core, and then you at least know how big a change the code will require. BTW, you didn't mention which thread was pegged. \$\endgroup\$ – J_H Aug 31 '17 at 16:11
3
\$\begingroup\$

I couldn't reproduce your performance problems, but I've got some suggestions for other improvements.

If you give the Ball class a dictionary similar to the options dict you can reduce the toggle_direction method to one line.

# In the __init__ method.
self.direction_dict = {
    Directions.UP_LEFT: Directions.UP_RIGHT,
    Directions.UP_RIGHT: Directions.UP_LEFT,
    Directions.DOWN_LEFT: Directions.DOWN_RIGHT,
    Directions.DOWN_RIGHT: Directions.DOWN_LEFT,
    }

# And then just set direction to the new direction.
def toggle_direction(self):
    self.direction = self.direction_dict[self.direction]

(You can define self.options in the __init__ method as well.)

However, I'd rather get rid of the Directions enum and these dicts and use vectors for the direction and positions instead. To move the ball you can just add its self.direction vector times the self.speed_up to the self.position and then update the self.rect. So the class could be written in this way:

class Ball(pygame.sprite.Sprite):

    def __init__(self, width, height):
        super().__init__()
        self.width, self.height = width, height
        self.image = pygame.Surface([10, 10])
        self.image.fill(WHITE)
        self.rect = self.image.get_rect()
        self.initialize()

    def initialize(self):
        """Reset the attributes of the ball."""
        self.direction = random.choice(
            [Vector2(-10, -10), Vector2(10, -10),
             Vector2(-10, 10), Vector2(10, 10)])
        self.position = Vector2(WIN_WIDTH/2, WIN_HEIGHT/2)
        self.rect.center = self.position
        self.hits = 0
        self.speed_up = 1.0

    def hit(self):
        self.hits += 1
        self.speed_up = 1.0 + self.hits/10

    def update(self):
        if self.position.y <= 10:  # upper border
            self.direction = random.choice([Vector2(-10, 10), Vector2(10, 10)])
        if self.position.y >= self.height - 10:  # bottom border
            self.direction = random.choice([Vector2(-10, -10), Vector2(10, -10)])

        self.position += self.direction * self.speed_up
        self.rect.center = self.position

I'd fuse the Player and Racket classes, since Player is just a container for some values and has got no methods, so using a class is unnecessary. But if you later want to add more logic you can keep them apart.


In Python you mostly don't need properties or getter and setter methods and can just assign or increment the attributes like score directly. You only need properties if you later have to add code that needs to be executed during the attribute access. The try...excepts are not really needed in your property methods.


Put all the sprites into a sprite group, so that you don't have to update and draw them separately in the main loop.


Don't instantiate pygame.SysFont each frame. Just define font = pygame.font.SysFont('Helvetica', 25) somewhere before the while loop.


Put your code into a main function and call it in a if __name__ == '__main__': clause, so that it doesn't run if the module gets imported. Leave only the constants in the global scope not the variables.


pygame.event.pump is not needed because you already have an event loop that clears the event queue.


Here's a complete example:

import random
import pygame
from pygame.math import Vector2


pygame.init()

FONT = pygame.font.SysFont('Helvetica', 25)
FPS = 30
WIN_WIDTH = 800
WIN_HEIGHT = 640
MAX_SCORE = 5
DISPLAY = (WIN_WIDTH, WIN_HEIGHT)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (64, 64, 64)


class Ball(pygame.sprite.Sprite):

    def __init__(self, width, height):
        super().__init__()
        self.width, self.height = width, height
        self.image = pygame.Surface([10, 10])
        self.image.fill(WHITE)
        self.rect = self.image.get_rect()
        self.initialize()

    def initialize(self):
        """Reset the attributes of the ball for a restart.

        Called when the ball leaves the screen and a player scores.
        """
        self.direction = random.choice(
            [Vector2(-10, -10), Vector2(10, -10),
             Vector2(-10, 10), Vector2(10, 10)])
        self.position = Vector2(WIN_WIDTH/2, WIN_HEIGHT/2)
        self.rect.center = self.position
        self.hits = 0
        self.speed_up = 1.0

    def hit(self):
        self.hits += 1
        self.speed_up = 1.0 + self.hits/10

    def update(self):
        if self.position.y <= 10:  # upper border
            self.direction = random.choice([Vector2(-10, 10), Vector2(10, 10)])
        if self.position.y >= self.height - 10:  # bottom border
            self.direction = random.choice([Vector2(-10, -10), Vector2(10, -10)])

        self.position += self.direction * self.speed_up
        self.rect.center = self.position


class Player(pygame.sprite.Sprite):

    def __init__(self, side, width, height):
        super().__init__()
        self.score = 0
        self.width, self.height = width, height
        self.racket_height = 100
        self.movement_speed = 20
        offset = 20
        self.image = pygame.Surface([10, self.racket_height])
        self.image.fill(WHITE)

        if side == 'Left':
            self.position = Vector2(offset, self.height/2)
        else:
            self.position = Vector2(self.width-offset-10, self.height/2)
        self.rect = self.image.get_rect(topleft=self.position)

    def move_up(self):
        if self.position.y > 0:
            self.position.y -= self.movement_speed
            self.rect.top = self.position.y

    def move_down(self):
        if self.position.y + self.racket_height < self.height:
            self.position.y += self.movement_speed
            self.rect.top = self.position.y


def game_over(screen, winner, left_paper, right_player):
    gray_overlay = pygame.Surface((WIN_WIDTH, WIN_HEIGHT))
    gray_overlay.fill(GRAY)
    gray_overlay.set_colorkey(GRAY)
    pygame.draw.rect(gray_overlay, BLACK, [0, 0, WIN_WIDTH, WIN_HEIGHT])
    gray_overlay.set_alpha(99)
    screen.blit(gray_overlay, (0, 0))
    font = pygame.font.SysFont(None, 100)
    game_over = font.render('{} Player WINS!'.format(winner), True, WHITE)
    screen.blit(game_over, (WIN_WIDTH / 2 - 300, WIN_HEIGHT / 2 - 100))
    scoreline = font.render(
        '{} - {}'.format(left_paper.score, right_player.score), True, WHITE)
    screen.blit(scoreline, (WIN_WIDTH / 2 - 50, WIN_HEIGHT / 2 + 100))
    pygame.display.update()
    pygame.time.delay(2000)


def render_score(left_player, right_player, font):
    """Render player scores onto surfaces."""
    left_player_score = font.render(str(left_player.score), True, (255, 255, 255))
    right_player_score = font.render(str(right_player.score), True, (255, 255, 255))
    return left_player_score, right_player_score


def main():
    screen = pygame.display.set_mode(DISPLAY, 0, 32)
    clock = pygame.time.Clock()

    left_player = Player('Left', WIN_WIDTH, WIN_HEIGHT)
    right_player = Player('Right', WIN_WIDTH, WIN_HEIGHT)
    curr_ball = Ball(WIN_WIDTH, WIN_HEIGHT)

    all_sprites = pygame.sprite.Group(left_player, right_player, curr_ball)

    goal_text = FONT.render(str(MAX_SCORE), True, (255, 255, 0))
    left_player_score, right_player_score = render_score(
        left_player, right_player, FONT)

    done = False

    while not done:
        # Event handling.
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                done = True

        keys = pygame.key.get_pressed()
        if keys[pygame.K_q]:
            done = True
        if keys[pygame.K_UP]:
            right_player.move_up()
        if keys[pygame.K_DOWN]:
            right_player.move_down()
        if keys[pygame.K_w]:
            left_player.move_up()
        if keys[pygame.K_s]:
            left_player.move_down()

        # Game logic.
        all_sprites.update()
        # Determine winner.
        if left_player.score >= MAX_SCORE or right_player.score >= MAX_SCORE:
            # This is a conditional expression (similar
            # to a ternary in other languages).
            winner = 'Left' if left_player.score > right_player.score else 'Right'
            game_over(screen, winner, left_player, right_player)
            done = True

        # Collision detection with the rackets/players.
        col_left = curr_ball.rect.colliderect(left_player.rect)
        col_right = curr_ball.rect.colliderect(right_player.rect)
        if col_right or col_left:
            curr_ball.direction.x *= -1  # Reverse the x component of the vectow.
            curr_ball.hit()

        if curr_ball.rect.x <= 0:  # left border
            right_player.score += 1
            curr_ball.initialize()
            left_player_score, right_player_score = render_score(
                left_player, right_player, FONT)
        elif curr_ball.rect.x >= WIN_WIDTH:  # right border
            left_player.score += 1
            curr_ball.initialize()
            left_player_score, right_player_score = render_score(
                left_player, right_player, FONT)

        # Drawing.
        screen.fill((30, 30, 70))
        screen.blit(left_player_score, (WIN_WIDTH / 2 - 100, 10))
        screen.blit(right_player_score, (WIN_WIDTH / 2 + 100, 10))
        screen.blit(goal_text, (WIN_WIDTH / 2, 0))
        all_sprites.draw(screen)

        pygame.display.set_caption('Ping Pong {}'.format(clock.get_fps()))

        pygame.display.flip()
        clock.tick(FPS)


if __name__ == '__main__':
    main()
    pygame.quit()
\$\endgroup\$
  • \$\begingroup\$ It would be nice to know if this improved the performance as well. Would using a Sprite Group give some improvement, aside from the other (more marginal?) improvements outlined above? \$\endgroup\$ – srattigan Aug 31 '17 at 19:03
  • \$\begingroup\$ I think the changes have hardly any effect on the performance. Both the original and the changed version run fine on my machine. The only thing that affected the performance in a negative way was the continuous creation of SysFont objects in the main loop, but that was only a minor problem. \$\endgroup\$ – skrx Aug 31 '17 at 19:23
  • \$\begingroup\$ Thanks a lot for the detailed code review. You (@skrx) brought out some important aspects to my attention. I issued the post mainly because i thought the event handling could be a bottleneck and my CPU fan immediately starts running the moment i start the application. I accepted your answer since it helped me a lot and the performance, by all appearances, cannot be further improved. \$\endgroup\$ – Tauling Sep 1 '17 at 9:33
  • \$\begingroup\$ I'm still wondering why you have this CPU problem. Does my example also cause 100% CPU usage? BTW, which Python and pygame version and operating system do you use? And how do you run the game? \$\endgroup\$ – skrx Sep 1 '17 at 12:03
  • 1
    \$\begingroup\$ The CPU usage did not change noticeable compared to the original version. I am executing the game using python3.5 on a linux mint directly through the CLI (pygame should be the current version). Nonetheless i gained a lot from your answer! The separation which is portrayed in your link looks very good. (BTW, sorry for my late response.) \$\endgroup\$ – Tauling Sep 11 '17 at 20:48
1
\$\begingroup\$

A simplification:

def toggle_direction(self):
    if self.direction == Directions.DOWN_LEFT:
        new_direction = Directions.DOWN_RIGHT

    if self.direction == Directions.DOWN_RIGHT:
        new_direction = Directions.DOWN_LEFT

    if self.direction == Directions.UP_RIGHT:
        new_direction = Directions.UP_LEFT

    if self.direction == Directions.UP_LEFT:
        new_direction = Directions.UP_RIGHT

    try:
        self.direction = new_direction
    except NameError:
        pass

Should be:

def toggle_direction(self):
    if self.direction == Directions.DOWN_LEFT:
        self.direction = Directions.DOWN_RIGHT

    elif self.direction == Directions.DOWN_RIGHT:
        self.direction = Directions.DOWN_LEFT

    elif self.direction == Directions.UP_RIGHT:
        self.direction = Directions.UP_LEFT

    elif self.direction == Directions.UP_LEFT:
        self.direction = Directions.UP_RIGHT

This is completely equivalent but simpler and avoids an exception handling call.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.