The goal of each frame of the animation to be the same length as the one before it, regardless of the amount of processing that takes place as the frame is being built.
This is a little animation demo that I wrote to show off the basics of a game loop in Python. With a newbies eyes in mind I have tried to keep to a limited tool kit; avoid pygame and threading and sticking to tkinter (included with most installations) to make the code as clear and as easy to understand as possible.
The end result works, but I am dissatisfied with the pacing on the game loop (onTimer). "After" isn't really to tool for the job.
In addition to being open to general comments, I am looking for suggestions to make the animation smoother and less at the whims of "after" and background event processing.
#Bounce
import tkinter
import time
import math
import random
#--- Initialize Globla Variables ----------------------------------------------
winWid = 640 #window size
winHei = 480
frameRate = 20 #length of a frame (include delay) in millesconds
frameDelay = 0 #the delay at the end of the current frame
frameStart = 0 #when the current frame started
balldiameter = 7
ballShape = []
ballX = []
ballY = []
spdX = []
spdY = []
ballCnt = 0
addTimer = time.perf_counter()
#--- Function/Tookkit list ----------------------------------------------------
def onTimer():
global timerhandle, frameStart, frameDelay, addTimer
#An animation frame is the work being done to draw/update a frame,
# plus the delay between the frames. As the work to draw the
# frame goes up, the delay between the frames goes down
elapsedTime = round((time.perf_counter() - frameStart)* 1000) #time from start of frame until now
frameDelay = frameRate - elapsedTime #delay for this frame is the frame size, minus how long it took to process this frame
if frameDelay < 1: frameDelay = 1 #bottom out with a single millesecond delay
frameStart = time.perf_counter() #start a new frame
#if the frame delay hasn't bottomed out and a half second has passed
if (time.perf_counter() - addTimer) > 0.25 and frameDelay > 1:
addTimer = time.perf_counter() #update the add time
addBall()
window.title("FD:" + str(frameDelay) + " - " + str(ballCnt))
moveTheBalls() #update the position of the balls
timerhandle = window.after(frameDelay,onTimer) #fill update rest of this frame with a delay
def onShutdown():
window.after_cancel(timerhandle)
window.destroy()
def addBall():
global ballCnt
newX = random.randrange(0,winWid)
newY = random.randrange(0,winHei)
color = randomColor()
ballShape.append(canvas.create_oval(newX,newY, newX+balldiameter,newY+balldiameter, outline=color, fill=color))
ballX.append(newX)
ballY.append(newY)
spdX.append((random.random() * 2)-1)
spdY.append((random.random() * 2)-1)
ballCnt = ballCnt + 1
def moveTheBalls():
for i in range(0,ballCnt): #for each ball
ballX[i] = ballX[i] + spdX[i] #update its position
ballY[i] = ballY[i] + spdY[i]
for j in range(i+1,ballCnt): #check for colision between other balls
dist = math.sqrt(( (ballX[i]+(balldiameter/2)) - (ballX[j]+(balldiameter/2)))**2 + ( (ballY[i]+(balldiameter/2)) - (ballY[j]+(balldiameter/2)))**2)
if dist < balldiameter: #if the balls are inside each other
hold = spdX[i] #swap their directions
spdX[i] = spdX[j]
spdX[j] = hold
hold = spdY[i]
spdY[i] = spdY[j]
spdY[j] = hold
if ballX[i] < 0 or ballX[i] > winWid-balldiameter: spdX[i] = spdX[i] * -1 #top or bottom? reverse directions
if ballY[i] < 0 or ballY[i] > winHei-balldiameter: spdY[i] = spdY[i] * -1 #left or right? reverse directions
canvas.coords(ballShape[i], (ballX[i], ballY[i], ballX[i]+balldiameter, ballY[i]+balldiameter))
#Random color - This is a helper function
#Returns a random color.
#Usage c4 = randomcolor()
def randomColor():
hexDigits = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
rclr = "#"
for i in range(0,6):
rclr = rclr + hexDigits[random.randrange(0,len(hexDigits))]
return rclr
#--- Main Program - Executes as soon as you hit run --------------------------
window = tkinter.Tk() #Sets the window
canvas = tkinter.Canvas(window, width=winWid, height=winHei, bg="white")
canvas.pack()
addBall()
timerhandle = window.after(20,onTimer) #start the game loop
window.protocol("WM_DELETE_WINDOW",onShutdown) #provide a graceful exit
window.mainloop() #Start the GUI