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
2 Answers 2
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()
-
\$\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– srattigan2017年08月31日 19:03:54 +00:00Commented 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\$skrx– skrx2017年08月31日 19:23:11 +00:00Commented 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\$Tauling– Tauling2017年09月01日 09:33:35 +00:00Commented 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\$skrx– skrx2017年09月01日 12:03:42 +00:00Commented 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\$Tauling– Tauling2017年09月11日 20:48:56 +00:00Commented Sep 11, 2017 at 20:48
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.
font
object each frame (do it once before the while loop). \$\endgroup\$