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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Aug 30, 2017 at 12:08
\$\endgroup\$
4
  • \$\begingroup\$ Please post the complete project, otherwise we can't run and profile the game. \$\endgroup\$ Commented Aug 30, 2017 at 23:21
  • \$\begingroup\$ Sorry. Now all the files are included. \$\endgroup\$ Commented Aug 31, 2017 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\$ Commented Aug 31, 2017 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\$ Commented Aug 31, 2017 at 16:11

2 Answers 2

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()
answered Aug 31, 2017 at 17:00
\$\endgroup\$
9
  • \$\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\$ Commented Aug 31, 2017 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\$ Commented Aug 31, 2017 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\$ Commented Sep 1, 2017 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\$ Commented Sep 1, 2017 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\$ Commented Sep 11, 2017 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.

answered Aug 31, 2017 at 12:00
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.