I am creating a simple pong game clone in tkinter and i want to create a game loop so that when the game is finished, it will restart.
import tkinter as tk
import random
import time
from time import sleep
# setting base Tk widget
root = tk.Tk()
root.resizable(False, False)
root.title("Pong")
root.configure(bg='black')
# finding the center of the screen
screen_w = root.winfo_screenwidth()
screen_h = root.winfo_screenheight()
canvas_w = 1280
canvas_h = 720
center_x = int(screen_w/2 - canvas_w / 2)
center_y = int(screen_h/2 - canvas_h / 2)
root.geometry(f'{canvas_w}x{canvas_h} {center_x} {center_y}')
def main():
# canvas
cv = tk.Canvas(root, width=canvas_w, height=canvas_h)
cv.delete('all')
cv.configure(bg="#000000")
global x, y, num_r, num_l, ball
# initializing variables
dirx = random.randint(0, 1)
diry = random.randint(0, 1)
x = 0
y = 0
num_l = 0
num_r = 0
if dirx == 0 and diry == 0:
x = -10
y = -10
if dirx == 0 and diry == 1:
x = -10
y = 10
if dirx == 1 and diry == 0:
x = 10
y = -10
if dirx == 1 and diry == 1:
x = 10
y = 10
#print(dirx, diry)
# paddles
paddle_l = cv.create_rectangle(20, 320, 30, 390, outline='#000000', fill='#ffffff')
paddle_r = cv.create_rectangle(1250, 320, 1260, 390, outline='#000000', fill='#ffffff')
# middle line
midline1 = cv.create_rectangle(canvas_w/2-5, 25, canvas_w/2 5, 85, outline='#000000', fill='#ffffff')
midline2 = cv.create_rectangle(canvas_w/2-5, 125, canvas_w/2 5, 185, outline='#000000', fill='#ffffff')
midline3 = cv.create_rectangle(canvas_w/2-5, 225, canvas_w/2 5, 285, outline='#000000', fill='#ffffff')
midline4 = cv.create_rectangle(canvas_w/2-5, 325, canvas_w/2 5, 385, outline='#000000', fill='#ffffff')
midline5 = cv.create_rectangle(canvas_w/2-5, 425, canvas_w/2 5, 485, outline='#000000', fill='#ffffff')
midline6 = cv.create_rectangle(canvas_w/2-5, 525, canvas_w/2 5, 585, outline='#000000', fill='#ffffff')
midline7 = cv.create_rectangle(canvas_w/2-5, 625, canvas_w/2 5, 685, outline='#000000', fill='#ffffff')
# score trackers
score_left = tk.Label(text='0', bg='#000000', fg='#ffffff', font=('Helvetica', 30))
score_right = tk.Label(text='0', bg='#000000', fg='#ffffff', font=('Helvetica', 30))
score_left.place(relx=0.43, rely=0.1)
score_right.place(relx=0.55, rely=0.1)
# ball
ball = cv.create_rectangle(canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10, outline='#000000', fill='#696969')
# movement of the paddles
def detectMoveKeys():
# left paddle
root.bind('w', leftUp)
root.bind('s', leftDown)
# right paddle
root.bind('i', rightUp)
root.bind('k', rightDown)
root.after(5, detectMoveKeys)
def leftUp(self):
cv.move(paddle_l, 0, -10)
#print('registered')
def leftDown(self):
cv.move(paddle_l, 0, 10)
#print('registered')
def rightUp(self):
cv.move(paddle_r, 0, -10)
#print('registered')
def rightDown(self):
cv.move(paddle_r, 0, 10)
#print('registered')
def ballMovement():
global x, y
if cv.coords(ball)[1] 10 == canvas_h:
y = y * (0-1)
print('ball touched bottom', x, y)
if cv.coords(ball)[1] == 0:
y = y * (0-1)
print('ball touched top', x, y)
pos = cv.coords(ball)
if paddle_l in cv.find_overlapping(pos[0], pos[1], pos[2], pos[3]):
x = x * (0-1)
print('ball touched left paddle', x, y)
if paddle_r in cv.find_overlapping(pos[0], pos[1], pos[2], pos[3]):
x = x * (0-1)
print('ball touched right paddle', x, y)
cv.move(ball, x, y)
#print(cv.coords(ball)[0])
cv.pack()
root.after(35, ballMovement)
def endGame():
if score_right['text'] == '10':
score_right['text'] = '0'
score_left['text'] = '0'
r_won = tk.Label(text='Right won,\nthe game will\nrestart in 3 sec.', bg='white', fg='black', font=('Helvetica', 50))
r_won.place(relx=0.3, rely=0.5)
cv.delete('all')
score_right.config(text='')
score_left.config(text='')
cv.destroy()
root.after(3000, main)
root.after(10, endGame)
def outOfBounds():
global num_l, num_r, ball, dirx, diry, x, y
# detects if the ball is out of bounds on the left side of the screen
if cv.coords(ball)[0] == -30.0:
print('right scored')
num_r = 1
score_right['text'] = str(num_r)
cv.pack()
cv.coords(ball, 630, 350, 650, 370)
#ball = cv.create_rectangle(canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10, fill='#696969')
dirx = random.randint(0, 1)
diry = random.randint(0, 1)
if dirx == 0 and diry == 0:
x = -10
y = -10
if dirx == 0 and diry == 1:
x = -10
y = 10
if dirx == 1 and diry == 0:
x = 10
y = -10
if dirx == 1 and diry == 1:
x = 10
y = 10
# detects if the ball is out of bounds on the right side of the screen
if cv.coords(ball)[0] == 1310.0:
print('left scored')
num_l = 1
score_left['text'] = str(num_l)
cv.pack()
cv.delete(ball)
ball = cv.create_rectangle(canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10, fill='#696969')
dirx = random.randint(0, 1)
diry = random.randint(0, 1)
if dirx == 0 and diry == 0:
x = -10
y = -10
if dirx == 0 and diry == 1:
x = -10
y = 10
if dirx == 1 and diry == 0:
x = 10
y = -10
if dirx == 1 and diry == 1:
x = 10
y = 10
root.after(10, outOfBounds)
root.after(5, detectMoveKeys)
root.after(35, ballMovement)
root.after(10, outOfBounds)
root.after(10, endGame)
main()
root.mainloop()
When I first start the program, everything runs fine until the first game loop is started. Then it starts lagging and eventually freezes.
Here are the error messages:
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib64/python3.10/tkinter/__init__.py", line 1921, in __call__
return self.func(*args)
File "/usr/lib64/python3.10/tkinter/__init__.py", line 839, in callit
func(*args)
File "/home/anton/Documents/pong/pong.py", line 140, in outOfBounds
if cv.coords(ball)[0] == -30.0:
File "/usr/lib64/python3.10/tkinter/__init__.py", line 2795, in coords
self.tk.call((self._w, 'coords') args))]
_tkinter.TclError: invalid command name ".!canvas"
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib64/python3.10/tkinter/__init__.py", line 1921, in __call__
return self.func(*args)
File "/usr/lib64/python3.10/tkinter/__init__.py", line 839, in callit
func(*args)
File "/home/anton/Documents/pong/pong.py", line 103, in ballMovement
if cv.coords(ball)[1] 10 == canvas_h:
File "/usr/lib64/python3.10/tkinter/__init__.py", line 2795, in coords
self.tk.call((self._w, 'coords') args))]
_tkinter.TclError: invalid command name ".!canvas"
What I have tried:
- Making sure all variables are set to their default values after the game start
- Putting everything in a tk.Frame widget and destroying that
If it helps I am running Fedora 36 Workstation.
Does anyone know what's the problem? Thanks in advance.
Edit:
I have implemented an 'end' flag so the loops stop, but the program starts lagging again after some restarts. I don't know how to fix this either.
...
cv = tk.Canvas(root, width=canvas_w, height=canvas_h)
def main():
#print('main called')
global x, y, num_r, num_l, ball, end
# canvas
cv.configure(bg="black")
...
end = False
...
def detectMoveKeys():
...
if end == False:
root.after(5, detectMoveKeys)
...
def ballMovement():
...
if end == False:
root.after(35, ballMovement)
def outOfBounds():
...
if end == False:
root.after(10, outOfBounds)
def endGame():
global end, ball
def r_won_destroy():
r_won.destroy()
cv.coords(ball, canvas_w/2-5, canvas_h/2-5, canvas_w/2 5, canvas_h/2 5)
cv.delete('all')
if score_right['text'] == '10':
score_right['text'] = ''
score_left['text'] = ''
r_won = tk.Label(text='Right won,\nthe game will\nrestart in 3 sec.', bg='white', fg='black', font=('Helvetica', 50))
r_won.place(relx=0.3, rely=0.5)
end = True
#cv.itemconfig('all', fill='black')
root.after(3000, main)
root.after(2000, r_won_destroy)
if end == False:
root.after(10, endGame)
if end == False:
root.after(5, detectMoveKeys)
root.after(35, ballMovement)
root.after(10, outOfBounds)
root.after(10, endGame)
main()
root.mainloop()
I am new to tkinter so sorry if this is an obvious mistake.
Can someone solve this, please?
CodePudding user response:
Part of the problem is in this block of code:
def endGame():
if score_right['text'] == '10':
...
root.after(3000, main)
root.after(10, endGame)
If the if
condition is true, you call main
after 3 seconds. You also continue to call endGame
100 times a second whether it's true or false So, even when the game ends, you're continuing to call endGame
100 times a second.
When you call main
from inside that if
block, main
also calls endGame
, causing endGame
to start a new loop of running 100 times a second. So after one restart you are calling endGame
100 times a second twice. The third restart you're calling endGame
100 times a second three times. And so on. Eventually you'll end up calling endGame
thousands of times a second, far more often than it can keep up with.
The same thing is happening for detectMoveKeys
, ballMovement
, and outOfBounds
. You never stop the endless loop of those functions when the game ends, and start a new endless loop every time a game starts.
CodePudding user response:
I think I did it. I may have just been calling the outOfBounds
, ballMovement
, detectMoveKeys
and endGame
too many times a second. (100 times) I have reduced that to 5 times a second and the lagging seems to be gone. I've tested 100 restarts and it was still responsive. But I feel like after an undefined amount of restarts, it's going to be lagging again, because the function calls may still be stacking on top of one another. Please tell me if this is the case. Thanks a lot for the help.
Here is the full code for you to test it out:
import tkinter as tk
import random
# setting base Tk widget
root = tk.Tk()
root.resizable(False, False)
root.title("Pong")
root.configure(bg='black')
# finding the center of the screen
screen_w = root.winfo_screenwidth()
screen_h = root.winfo_screenheight()
canvas_w = 1280
canvas_h = 720
center_x = int(screen_w/2 - canvas_w / 2)
center_y = int(screen_h/2 - canvas_h / 2)
root.geometry(f'{canvas_w}x{canvas_h} {center_x} {center_y}')
# canvas
cv = tk.Canvas(root, width=canvas_w, height=canvas_h)
cv.configure(bg="black")
# paddles
paddle_l = cv.create_rectangle(20, 320, 30, 390, outline='#000000', fill='#ffffff')
paddle_r = cv.create_rectangle(1250, 320, 1260, 390, outline='#000000', fill='#ffffff')
# middle line
midline1 = cv.create_rectangle(canvas_w/2-5, 25, canvas_w/2 5, 85, outline='#000000', fill='#ffffff')
midline2 = cv.create_rectangle(canvas_w/2-5, 125, canvas_w/2 5, 185, outline='#000000', fill='#ffffff')
midline3 = cv.create_rectangle(canvas_w/2-5, 225, canvas_w/2 5, 285, outline='#000000', fill='#ffffff')
midline4 = cv.create_rectangle(canvas_w/2-5, 325, canvas_w/2 5, 385, outline='#000000', fill='#ffffff')
midline5 = cv.create_rectangle(canvas_w/2-5, 425, canvas_w/2 5, 485, outline='#000000', fill='#ffffff')
midline6 = cv.create_rectangle(canvas_w/2-5, 525, canvas_w/2 5, 585, outline='#000000', fill='#ffffff')
midline7 = cv.create_rectangle(canvas_w/2-5, 625, canvas_w/2 5, 685, outline='#000000', fill='#ffffff')
# score trackers
score_left = tk.Label(text='0', bg='#000000', fg='#ffffff', font=('Helvetica', 30))
score_right = tk.Label(text='0', bg='#000000', fg='#ffffff', font=('Helvetica', 30))
score_left.place(relx=0.43, rely=0.1)
score_right.place(relx=0.55, rely=0.1)
# ball
ball = cv.create_rectangle(canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10, outline='#000000', fill='#696969')
cv.pack()
# movement of the paddles
def detectMoveKeys():
# left paddle
root.bind('w', leftUp)
root.bind('s', leftDown)
# right paddle
root.bind('i', rightUp)
root.bind('k', rightDown)
if end == False:
root.after(200, detectMoveKeys)
def leftUp(self):
cv.move(paddle_l, 0, -10)
#print('registered')
def leftDown(self):
cv.move(paddle_l, 0, 10)
#print('registered')
def rightUp(self):
cv.move(paddle_r, 0, -10)
#print('registered')
def rightDown(self):
cv.move(paddle_r, 0, 10)
#print('registered')
def ballMovement():
global x, y
# checking if the ball touched the bottom side of the window
if cv.coords(ball)[1] 10 == canvas_h:
y = y * (0-1)
print('ball touched bottom', x, y)
# checking if the ball touched the top side of the window
if cv.coords(ball)[1] == 0:
y = y * (0-1)
print('ball touched top', x, y)
pos = cv.coords(ball)
if paddle_l in cv.find_overlapping(pos[0], pos[1], pos[2], pos[3]):
x = x * (0-1)
print('ball touched left paddle', x, y)
if paddle_r in cv.find_overlapping(pos[0], pos[1], pos[2], pos[3]):
x = x * (0-1)
print('ball touched right paddle', x, y)
cv.move(ball, x, y)
#print(cv.coords(ball)[0])
cv.pack()
if end == False:
root.after(35, ballMovement)
def outOfBounds():
global num_l, num_r, ball, dirx, diry, x, y
# checking if the ball is out of bounds on the left side
if cv.coords(ball)[0] <= -30.0:
print('right scored')
num_r = 1
score_right['text'] = str(num_r)
# setting the ball to the original position and setting the direction
cv.coords(ball, canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10)
cv.pack()
dirx = random.randint(0, 1)
diry = random.randint(0, 1)
if dirx == 0 and diry == 0:
x = -10
y = -10
if dirx == 0 and diry == 1:
x = -10
y = 10
if dirx == 1 and diry == 0:
x = 10
y = -10
if dirx == 1 and diry == 1:
x = 10
y = 10
# checking if the ball is out of bounds on the right side
if cv.coords(ball)[0] >= 1310.0:
print('left scored')
num_l = 1
score_left['text'] = str(num_l)
# setting the ball to the original position and setting the direction
cv.coords(ball, canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10)
cv.pack()
dirx = random.randint(0, 1)
diry = random.randint(0, 1)
if dirx == 0 and diry == 0:
x = -10
y = -10
if dirx == 0 and diry == 1:
x = -10
y = 10
if dirx == 1 and diry == 0:
x = 10
y = -10
if dirx == 1 and diry == 1:
x = 10
y = 10
if end == False:
root.after(200, outOfBounds)
# checking if the game should end
def endGame():
global end, ball, paddle_l, paddle_r
# deleting the label and settings objects back to original positions
def r_won_destroy():
r_won.destroy()
cv.coords(ball, canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10)
cv.coords(paddle_l, 20, 320, 30, 390)
cv.coords(paddle_r, 1250, 320, 1260, 390)
score_right['text'] = '0'
score_left['text'] = '0'
# checking if the right score is 10, if yes, the game ends
if score_right['text'] == '10':
r_won = tk.Label(text='Right won,\nthe game will\nrestart in 3 sec.', bg='white', fg='black', font=('Helvetica', 50))
r_won.place(relx=0.3, rely=0.5)
end = True
#cv.itemconfig('all', fill='black')
root.after(3000, main)
root.after(2000, r_won_destroy)
# deleting the label and settings objects back to original positions
def l_won_destroy():
l_won.destroy()
cv.coords(ball, canvas_w/2-10, canvas_h/2-10, canvas_w/2 10, canvas_h/2 10)
cv.coords(paddle_l, 20, 320, 30, 390)
cv.coords(paddle_r, 1250, 320, 1260, 390)
score_right['text'] = '0'
score_left['text'] = '0'
# checking if the left score is 10, if yes, the game ends
if score_left['text'] == '10':
l_won = tk.Label(text='Left won,\nthe game will\nrestart in 3 sec.', bg='white', fg='black', font=('Helvetica', 50))
l_won.place(relx=0.3, rely=0.5)
end = True
#cv.itemconfig('all', fill='black')
root.after(3000, main)
root.after(2000, l_won_destroy)
if end == False:
root.after(200, endGame)
def main():
# initializing variables
global x, y, end, num_l, num_r
end = False
num_l = 0
num_r = 0
x = 0
y = 0
dirx = random.randint(0, 1)
diry = random.randint(0, 1)
# setting the starting diretion of the ball
if dirx == 0 and diry == 0:
x = -10
y = -10
if dirx == 0 and diry == 1:
x = -10
y = 10
if dirx == 1 and diry == 0:
x = 10
y = -10
if dirx == 1 and diry == 1:
x = 10
y = 10
# calling other functions
detectMoveKeys()
ballMovement()
outOfBounds()
endGame()
# calling the main function
main()
# starting mainloop for the main window object
root.mainloop()