user9872569
user9872569

Reputation:

Python : Tkinter lag

I have a question . I'm making a platformer game in tkinter and I have an issue : I have for now : player , blocks and coins . I'm updating the player's move and it's animation and the coin's animation and for some reason when I'm putting too much coins , the player's movement starts lagging. Note: I'm using the after function of tkinter for animations for player's movement + animation and same goes for the coins . For other things like gravity and etc I'm using just threads .

code of coins updating :

def coinsCheckCollision(self):
    cRemove = None
    indexRemove = -1
    count = 0
    for c in self.frame.coins:
        x, y , width , height = c.getRectangle()
        xP = self.player.getX; yP = self.player.getY; wP = self.player.getWidth; hP = self.player.getHeight
        if collisionDetect(xP , x, yP  , y, wP , width, hP , height) or collisionDetect(x , xP , y , yP , width , wP , height , hP):
            if count not in coinsRemoved:
                indexRemove = count
        if indexRemove != -1:
            if indexRemove not in coinsRemoved:
                coinsRemoved.append(indexRemove)
        count +=1

def coinsUpdateAnimations(self):
    count = 0
    for c in self.frame.coins:
        if count not in coinsRemoved:
            self.img = c.getAnimation()
            self.img = ImageTk.PhotoImage(self.img)
            self.frame.coinsImages[count] = self.img
        else:
            if self.frame.coinsImages[count] is not '' :
                self.frame.coinsImages[count] = ''
                self.frame.canvas.delete('coinB'+str(count))
        what = self.frame.canvas.itemconfig('coin' + str(count), image=self.frame.coinsImages[count])
        count += 1
    self.coinsCheckCollision()
    self.frame.frame.after(40 , self.coinsUpdateAnimations)

Anyway , the question in short is : why when I'm updating multiple things that aren't really "related" to each other , the gui starts lagging ?

Upvotes: 4

Views: 3723

Answers (1)

abarnert
abarnert

Reputation: 365717

Your design seems to expect your functions to run every 40ms. Maybe +/- a few ms, but averaging 25 times per second.

But that's not what happens.


First, how many coins do you have, and how complicated is that collisionDetect function? If it only takes a tiny fraction of 1ms to run through that loop, it's no big deal, but think about what happens if it takes, say, 15ms: You wait 40ms, then do 15ms of work, then wait another 40ms, then do 15ms of work, etc. So your work is running only 15 times per second, instead of 25.

Now imagine each coin takes, say, 0.2ms. At 3 coins, there's a lag of 0.6ms, which is barely noticeably. But at 100 coins, there's a lag of 20ms. That slows the coins down by 50%, which is pretty obviously noticeable.


Second, as the docs say:

Tkinter only guarantees that the callback will not be called earlier than that; if the system is busy, the actual delay may be much longer.

Being off a few ms randomly in either direction might be fine; it would all average out in the end. But after is always a few ms late, never a few ms early, so instead of averaging out, it just builds up and you get further and further behind.

And, worse, if one of your functions gets behind, it will tend to make the delay in each after a bit longer—so it won't just be your coin animation slowing down 50%, but the whole game slowing down by some unpredictable amount arbitrarily between 0-50%, but probably enough to be noticeable.


To solve both problems, you need to carry around something like the time you expected to run at, then, instead of doing after(40), you do something like this:

expected_time += 40
delay = expected_time - current_time
after(max(0, delay), func)

To put it in concrete (although untested) terms, using the datetime module:

def __init__(self):
    self.next_frame_time = datetime.datetime.now()
    self.schedule()

def schedule(self):
    self.next_frame_time += datetime.timedelta(seconds=0.040)
    now = datetime.datetime.now()
    delta = max(datetime.timedelta(), now - self.next_frame_time)
    self.frame.frame.after(delta.total_seconds * 1000, self.coinsUpdateAnimations)

def coinsUpdateAnimations(self):
    # all the existing code before the last two lines
    self.coinsCheckCollision()
    self.schedule()

This still won't solve things if the total work you do takes more than 40ms, of course. Imagine that you spend 50ms, then do an after(0, func), which triggers at least 10ms late, and then spend another 50ms, then the next after(0, func) triggers at least 20ms late, and so on. If you can't do all of your work in something that's usually significantly less than 40ms, you won't be able to keep up. You have to either:

  • Find a way to optimize your code (e.g., maybe you can use a better algorithm, or use numpy instead of a for loop),
  • Redesign your game to do less work, or
  • Slow down your frame rate to something you actually can keep up with.

A possibly better solution is to stop trying to bend Tkinter into a gaming framework. It's not designed for that, doesn't help you get all the fiddly details right, and doesn't work all that well even once you do get them right.

By contrast, something like Pygame Zero is, as the name implies, designed for creating games. And designed to make it easy enough that people with a lot less Python experience than you seem to have can use it.

For example, instead of an event loop that runs at whatever speed your OS wants to run it, making it your responsibility to get everything timed right, Pygame Zero runs a frame loop that calls your update function N times per second, as close to evenly as possible. And it has built-in functions for things like collision detection, drawing animated sprites, etc.

Upvotes: 4

Related Questions