To kick off the year 2024 on SSaurel’s Blog, what better way than to code the classic game of Pong in Python?
That’s what I suggest you do in this step-by-step tutorial, using the Tkinter library for the user interface. This will show you how to program a simple game in Python and manage user interaction.
Let’s start by designing the Ball
To design our ball, we need to define its position and velocity. We’ll then need to define a method for modifying the position of the rectangle representing this ball as its coordinates change.
When the ball touches one of the two horizontal edges, we’ll need to restart the ball from the center of the screen. This will be done in a restart method, and we’ll use the random method in Python’s Random library to randomly choose an x and y direction.
Finally, the bulk of the work remains to be done in the ball’s move method.
We’ll have to deal with several scenarios. The first is when the ball touches the outer edges at height. In this case, we’ll need to reverse the direction of the ball in ordinate. This is done by multiplying the current y velocity of the ball by -1.
The second is when the ball comes into contact with the right or left paddles. In this case, you need to invert the current velocity on the x-axis. This is done by multiplying the velocity at x by -1.
In the third and fourth cases, points are scored for the player on the left and the player on the right. If the ball touches the edge on the left, the player on the right scores 1 point. If the ball touches the edge on the right, the player on the left scores 1 point.
Finally, we play the x and y positions of the ball, adding the current velocity.
This gives us the following for the Ball object:
# First, we design the Ball Object | |
class Ball: | |
def __init__(self, canvas, width, velocity, boardwidth, boardheight): | |
self.width = width | |
self.boardwidth = boardwidth | |
self.boardheight = boardheight | |
# we center the ball on the board | |
self.topx = boardwidth / 2 - width / 2 | |
self.topy = boardheight / 2 - width / 2 | |
self.velocity = velocity | |
self.vx = velocity | |
self.vy = velocity | |
self.canvas = canvas | |
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.width, fill = 'white') | |
# we define a method to draw the ball on the canvas | |
def draw(self): | |
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.width) | |
# we define a restart method for restarting the ball move | |
def restart(self): | |
self.topx = self.boardwidth / 2 - self.width / 2 | |
self.topy = self.boardheight / 2 - self.width / 2 | |
# we define a random direction for the ball when restarting | |
self.vx = (-1, 1)[rand.random() > 0.5] * self.velocity | |
self.vy = (-1, 1)[rand.random() > 0.5] * self.velocity | |
# Move the ball | |
# we need to pass the pong game instance and the paddles in the move method of the Ball. You can improve this by yourself later ;) | |
def move(self, pong, paddleright, paddleleft): | |
# if the ball touches the top or the bottom of the board, we invert direction y | |
if self.topy <= 0 or (self.topy + self.width) >= self.boardheight: | |
self.vy = self.vy * -1 | |
# if the ball touches one of both paddles, we invert direction x | |
if paddleright.collideright(self) or paddleleft.collideleft(self): | |
self.vx = self.vx * -1 | |
# if the ball touches the right or the left of the board, we update paddle points and we return True | |
if (self.topx + self.width) >= self.boardwidth: | |
pong.leftpoints = pong.leftpoints + 1 | |
return True | |
if self.topx <= 0: | |
pong.rightpoints = pong.rightpoints + 1 | |
return True | |
# we update ball position | |
self.topx = self.topx + self.vx | |
self.topy = self.topy + self.vy | |
return False |
Let’s move on to designing the Paddle
To model a paddle, we define a Paddle object.
This Paddle object will have x and y coordinates, as well as a width and height. Each paddle is assigned a score initialized to 0. We also initially create the rectangle representing the paddle and store its id returned by the Tkinter library’s create_rectangle method. This id will then be used to modify the paddle’s coordinates.
The draw method is used to modify the paddle’s coordinates using this id.
The top and down methods move the paddle up and down respectively. The VELOCITY constant is used for displacement. This constant will also be used whenever ball positions are modified.
Finally, we use the collideright and collideleft methods to test whether the paddle and ball collide to the right and left respectively. In the event of a collision, we return True, which will then be used in the Pong class we’ll define for the Pong game.
This gives us the following code for our Paddle object:
# Now, it is time to design the Paddle for our Pong Game | |
class Paddle: | |
def __init__(self, canvas, topx, topy, width, height, boardheight): | |
self.topx = topx | |
self.topy = topy | |
self.width = width | |
self.height = height | |
self.boardheight = boardheight | |
self.score = 0 | |
self.canvas = canvas | |
# draw this paddle according positions passed in parameter | |
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.height, fill = 'white') | |
# we update coords of this paddle | |
def draw(self): | |
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.height) | |
# now, we need to manage down event then top event for the current paddle object | |
def top(self): | |
if self.topy - VELOCITY > 0: | |
self.topy = self.topy - VELOCITY | |
def down(self): | |
if (self.topy + self.height + VELOCITY) < self.boardheight: | |
self.topy = self.topy + VELOCITY | |
# use both methods to collide paddle right or left. As an exercise, you can improve this to make one generic method ;) | |
def collideright(self, ball): | |
if (ball.topx + ball.width) >= self.topx and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)): | |
return True | |
return False | |
def collideleft(self, ball): | |
if ball.topx <= (self.topx + self.width) and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)): | |
return True | |
return False |
Time to design the Pong Game using Ball and Paddle objects
With our two objects modeled above, we can move on to modeling the Pong object.
The Pong object will store the scores of the two Paddles, as well as the objects drawn on the canvas to represent these scores. These will be deleted before each rewrite when the scores are updated. This avoids having to delete the entire canvas each time the scores are updated.
A Boolean is set to True when the game is rendering.
Four Booleans are used to define whether the up/down keys chosen for each player are pressed or not. If they are pressed, the paddles are activated. The Paddle and Ball objects are created in the Pong object constructor. We call the canvas’s pack method and then call the drawmiddlelines, drawbord, and move methods respectively to start the game.
The drawmiddlelines method draws the mid-screen line vertically.
The drawboard method draws all game elements. Note the small workaround used with the _exit method of the os library to ensure that the application stops when the Tkinter game window is closed.
The drawpoints method is used to draw the scores of two players on the screen.
The move method will be called every X milliseconds to update the state of our Pong game. We’ll use a Timer object from Python’s Threading library to handle this repetitive call. In the method, we check whether it’s necessary to move the paddles. If so, we act accordingly.
After calling the ball’s move method, we retrieve the returned state. If the state is True, this means that the ball must be restarted from the center of the board. To do this, we call the restart method of the Pong object, within which we call only the restart method of the Ball object. So why a separate method? To improve the game in the future and add more behaviors to restart. You could choose to emit a sound at this point…
As for the keypress and keyrelease methods, they are associated with the keyboard events and . This makes it possible to manage several keyboard keys at the same time. We choose the z / s keys for the left paddle and the o / l keys for the right paddle.
Finally, the killtimer method will be used to end the rendering phase and cancel the timer if it’s running.
This gives us the following code for the Pong object:
# Now, we can define the Pong Game Object | |
class Pong: | |
def __init__(self, root, width, height, margin): | |
paddlewidth = width / 50 | |
paddleheight = height / 12 | |
self.leftpoints = 0 | |
self.lefttxt = None | |
self.rightpoints = 0 | |
self.righttxt = None | |
self.render = True # True when we need to render the game on the canvas | |
# we manage left up / down for moving the left paddle | |
self.leftup = False | |
self.leftdown = False | |
# same for right paddle | |
self.rightup = False | |
self.rightdown = False | |
self.width = width | |
self.height = height | |
self.margin = margin | |
self.root = root | |
self.root.title("Pong Game - SSaurel's Blog") | |
self.root.geometry(str(width) + "x" + str(height)) | |
# we create the canvas | |
self.canvas = tk.Canvas(self.root, width = width, height = height, bg = 'black') | |
self.paddleleft = Paddle(self.canvas, margin, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height) | |
self.paddleright = Paddle(self.canvas, (width - margin) - paddlewidth, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height) | |
self.ball = Ball(self.canvas, paddlewidth, VELOCITY, width, height) | |
self.canvas.pack() | |
self.drawmiddlelines() | |
self.drawboard() | |
self.move() | |
# we move draw middle lines for the board | |
def drawmiddlelines(self): | |
leftx = self.width / 2 - self.paddleleft.width / 2 | |
for y in range(0, self.height, int(self.paddleleft.height + self.margin * 2)): | |
self.canvas.create_rectangle(leftx, y, leftx + self.paddleleft.width, y + self.paddleleft.height, fill = 'grey') | |
def drawboard(self): | |
try: | |
# draw the paddles | |
self.paddleleft.draw() | |
self.paddleright.draw() | |
# draw points | |
self.drawpoints() | |
# draw the ball | |
self.ball.draw() | |
except: | |
# some strange exception occur here when we quit the game. We need to call explicitly exit! | |
os._exit(0) | |
def drawpoints(self): | |
# we delete the previous score for the left paddle | |
if self.lefttxt != None: | |
self.canvas.delete(self.lefttxt) | |
# we write the new score | |
self.lefttxt = self.canvas.create_text(self.width / 2 - 50, 50, text = str(self.leftpoints), fill = 'grey', font = ("Helvetica 35 bold")) | |
# the same thing for the right paddle | |
if self.righttxt != None: | |
self.canvas.delete(self.righttxt) | |
# we write the new score | |
self.righttxt = self.canvas.create_text(self.width / 2 + 50, 50, text = str(self.rightpoints), fill = 'grey', font = ("Helvetica 35 bold")) | |
# we define the move method to update the game elements | |
def move(self): | |
if self.render: | |
# use a timer to call this method each X milliseconds | |
self.timer = threading.Timer(0.05, self.move) | |
self.timer.start() | |
# we manage touch events | |
if self.leftup: | |
self.paddleleft.top() | |
if self.leftdown: | |
self.paddleleft.down() | |
if self.rightup: | |
self.paddleright.top() | |
if self.rightdown: | |
self.paddleright.down() | |
# True if the Ball touched one of both sides of the board | |
state = self.ball.move(self, self.paddleright, self.paddleleft) | |
if state: | |
self.restart() # we need to restart the ball | |
self.drawboard() | |
def restart(self): | |
self.ball.restart() | |
# Time to manage keyboards event from users | |
# We need to make this special code to detect several keys used at the same time on the keyboard | |
# z / s for the left paddle - o / l for the right paddle | |
def keypress(self, event): | |
match event.char: | |
case 'z': | |
self.leftup = True | |
case 's': | |
self.leftdown = True | |
case 'o': | |
self.rightup = True | |
case 'l': | |
self.rightdown = True | |
def keyrelease(self, event): | |
match event.char: | |
case 'z': | |
self.leftup = False | |
case 's': | |
self.leftdown = False | |
case 'o': | |
self.rightup = False | |
case 'l': | |
self.rightdown = False | |
# last method: we define a method to kill the timer and stop the rendering of the game | |
def killtimer(self): | |
self.render = False | |
self.timer.cancel() | |
self.root.destroy() |
Assembling objects for the Pong Game
The final part of the program simply consists of creating the root of the UI by calling the TK method of the tk object. The instance of the Pong game is created by passing the board size and margin as parameters. These are defined as constants at the start of the program.
We then bind the and events to the UI root and call the keypress and keyrelease methods of our Pong object instance respectively. Finally, don’t forget to listen for the WM_DELETE_WINDOW event to react to the click on the cross in our Tkinter window. This ensures that the timer is properly killed and that the process associated with our Pong game is stopped.
This gives us the following complete code for our Pong game in Python with a Tkinter UI:
import tkinter as tk | |
import random as rand | |
import threading | |
import os | |
#define some constants | |
WIDTH = 1000 | |
HEIGHT = 700 | |
MARGIN = 15 | |
VELOCITY = 15 | |
# First, we design the Ball Object | |
class Ball: | |
def __init__(self, canvas, width, velocity, boardwidth, boardheight): | |
self.width = width | |
self.boardwidth = boardwidth | |
self.boardheight = boardheight | |
# we center the ball on the board | |
self.topx = boardwidth / 2 - width / 2 | |
self.topy = boardheight / 2 - width / 2 | |
self.velocity = velocity | |
self.vx = velocity | |
self.vy = velocity | |
self.canvas = canvas | |
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.width, fill = 'white') | |
# we define a method to draw the ball on the canvas | |
def draw(self): | |
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.width) | |
# we define a restart method for restarting the ball move | |
def restart(self): | |
self.topx = self.boardwidth / 2 - self.width / 2 | |
self.topy = self.boardheight / 2 - self.width / 2 | |
# we define a random direction for the ball when restarting | |
self.vx = (-1, 1)[rand.random() > 0.5] * self.velocity | |
self.vy = (-1, 1)[rand.random() > 0.5] * self.velocity | |
# Move the ball | |
# we need to pass the pong game instance and the paddles in the move method of the Ball. You can improve this by yourself later ;) | |
def move(self, pong, paddleright, paddleleft): | |
# if the ball touches the top or the bottom of the board, we invert direction y | |
if self.topy <= 0 or (self.topy + self.width) >= self.boardheight: | |
self.vy = self.vy * -1 | |
# if the ball touches one of both paddles, we invert direction x | |
if paddleright.collideright(self) or paddleleft.collideleft(self): | |
self.vx = self.vx * -1 | |
# if the ball touches the right or the left of the board, we update paddle points and we return True | |
if (self.topx + self.width) >= self.boardwidth: | |
pong.leftpoints = pong.leftpoints + 1 | |
return True | |
if self.topx <= 0: | |
pong.rightpoints = pong.rightpoints + 1 | |
return True | |
# we update ball position | |
self.topx = self.topx + self.vx | |
self.topy = self.topy + self.vy | |
return False | |
# Now, it is time to design the Paddle for our Pong Game | |
class Paddle: | |
def __init__(self, canvas, topx, topy, width, height, boardheight): | |
self.topx = topx | |
self.topy = topy | |
self.width = width | |
self.height = height | |
self.boardheight = boardheight | |
self.score = 0 | |
self.canvas = canvas | |
# draw this paddle according positions passed in parameter | |
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.height, fill = 'white') | |
# we update coords of this paddle | |
def draw(self): | |
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.height) | |
# now, we need to manage down event then top event for the current paddle object | |
def top(self): | |
if self.topy - VELOCITY > 0: | |
self.topy = self.topy - VELOCITY | |
def down(self): | |
if (self.topy + self.height + VELOCITY) < self.boardheight: | |
self.topy = self.topy + VELOCITY | |
# use both methods to collide paddle right or left. As an exercise, you can improve this to make one generic method ;) | |
def collideright(self, ball): | |
if (ball.topx + ball.width) >= self.topx and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)): | |
return True | |
return False | |
def collideleft(self, ball): | |
if ball.topx <= (self.topx + self.width) and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)): | |
return True | |
return False | |
# Now, we can define the Pong Game Object | |
class Pong: | |
def __init__(self, root, width, height, margin): | |
paddlewidth = width / 50 | |
paddleheight = height / 12 | |
self.leftpoints = 0 | |
self.lefttxt = None | |
self.rightpoints = 0 | |
self.righttxt = None | |
self.render = True # True when we need to render the game on the canvas | |
# we manage left up / down for moving the left paddle | |
self.leftup = False | |
self.leftdown = False | |
# same for right paddle | |
self.rightup = False | |
self.rightdown = False | |
self.width = width | |
self.height = height | |
self.margin = margin | |
self.root = root | |
self.root.title("Pong Game - SSaurel's Blog") | |
self.root.geometry(str(width) + "x" + str(height)) | |
# we create the canvas | |
self.canvas = tk.Canvas(self.root, width = width, height = height, bg = 'black') | |
self.paddleleft = Paddle(self.canvas, margin, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height) | |
self.paddleright = Paddle(self.canvas, (width - margin) - paddlewidth, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height) | |
self.ball = Ball(self.canvas, paddlewidth, VELOCITY, width, height) | |
self.canvas.pack() | |
self.drawmiddlelines() | |
self.drawboard() | |
self.move() | |
# we move draw middle lines for the board | |
def drawmiddlelines(self): | |
leftx = self.width / 2 - self.paddleleft.width / 2 | |
for y in range(0, self.height, int(self.paddleleft.height + self.margin * 2)): | |
self.canvas.create_rectangle(leftx, y, leftx + self.paddleleft.width, y + self.paddleleft.height, fill = 'grey') | |
def drawboard(self): | |
try: | |
# draw the paddles | |
self.paddleleft.draw() | |
self.paddleright.draw() | |
# draw points | |
self.drawpoints() | |
# draw the ball | |
self.ball.draw() | |
except: | |
# some strange exception occur here when we quit the game. We need to call explicitly exit! | |
os._exit(0) | |
def drawpoints(self): | |
# we delete the previous score for the left paddle | |
if self.lefttxt != None: | |
self.canvas.delete(self.lefttxt) | |
# we write the new score | |
self.lefttxt = self.canvas.create_text(self.width / 2 - 50, 50, text = str(self.leftpoints), fill = 'grey', font = ("Helvetica 35 bold")) | |
# the same thing for the right paddle | |
if self.righttxt != None: | |
self.canvas.delete(self.righttxt) | |
# we write the new score | |
self.righttxt = self.canvas.create_text(self.width / 2 + 50, 50, text = str(self.rightpoints), fill = 'grey', font = ("Helvetica 35 bold")) | |
# we define the move method to update the game elements | |
def move(self): | |
if self.render: | |
# use a timer to call this method each X milliseconds | |
self.timer = threading.Timer(0.05, self.move) | |
self.timer.start() | |
# we manage touch events | |
if self.leftup: | |
self.paddleleft.top() | |
if self.leftdown: | |
self.paddleleft.down() | |
if self.rightup: | |
self.paddleright.top() | |
if self.rightdown: | |
self.paddleright.down() | |
# True if the Ball touched one of both sides of the board | |
state = self.ball.move(self, self.paddleright, self.paddleleft) | |
if state: | |
self.restart() # we need to restart the ball | |
self.drawboard() | |
def restart(self): | |
self.ball.restart() | |
# Time to manage keyboards event from users | |
# We need to make this special code to detect several keys used at the same time on the keyboard | |
# z / s for the left paddle - o / l for the right paddle | |
def keypress(self, event): | |
match event.char: | |
case 'z': | |
self.leftup = True | |
case 's': | |
self.leftdown = True | |
case 'o': | |
self.rightup = True | |
case 'l': | |
self.rightdown = True | |
def keyrelease(self, event): | |
match event.char: | |
case 'z': | |
self.leftup = False | |
case 's': | |
self.leftdown = False | |
case 'o': | |
self.rightup = False | |
case 'l': | |
self.rightdown = False | |
# last method: we define a method to kill the timer and stop the rendering of the game | |
def killtimer(self): | |
self.render = False | |
self.timer.cancel() | |
self.root.destroy() | |
# we can assemble the pong game elements! | |
root = tk.Tk() | |
pong = Pong(root, WIDTH, HEIGHT, MARGIN) | |
# we bind key press and key release events to our Pong Game Object | |
root.bind("<KeyPress>", pong.keypress) | |
root.bind("<KeyRelease>", pong.keyrelease) | |
# we listen to WM_DELETE_WINDOW event to kill the timer ... | |
root.wm_protocol("WM_DELETE_WINDOW", pong.killtimer) | |
root.mainloop() | |
# Time to try our Pong Game in Python with Tkinter | |
# Improvement you can make from here : | |
# - Make a limit points to end the game. For the moment, it is an infinite Pong Game :D | |
# - You can also imagine to increase the velocity of the ball each 10 points scored. | |
# - You can implement an AI for the second paddle | |
# - You can improve the design of the objects as stated in the code | |
# --> It is your turn to code :D |
You can see the result of our Pong Game below:
You can also watch this tutorial on YouTube on the SSaurel channel here:
At the end of the program code on Github, I’ve also suggested some ideas for improvements, such as defining a limit at which the game is over. You can also define an AI for the second paddle. Finally, there’s inevitably some work to be done to improve the dependencies between each of the objects. This will allow you to code your own Python Pong game.
It’s up to you to code.
Top comments (0)