killereks
killereks

Reputation: 189

Python tkinter performance issue

I wanted to create a block physics based game, but the fps drops down really fast. I tried to optimise it as much as I could and it is still pretty low fps (200 block give me about 20 fps). Does anyone have any idea how to optimise my block game ? I was planning to add more but seeing this problem i might give up.

This is my code:

from tkinter import *
import math
import random
import time


master = Tk()

w = Canvas(master,width=600,height=600)
w.pack()

tileSize = 20

def randomColor():
    r = math.floor(random.random() * 255)
    g = math.floor(random.random() * 255)
    b = math.floor(random.random() * 255)
    return '#%02x%02x%02x' % (r, g, b)

class newElement:
    def __init__(self,x,y):
        x = math.floor(x/tileSize) * tileSize
        y = math.floor(y/tileSize) * tileSize
        self.x = x
        self.y = y
        self.vx = 0
        self.vy = 0
        self.width = tileSize
        self.height = tileSize
        self.id = random.random()
        self.color = randomColor()

elements = []

def mouse(event):
    global mouseX,mouseY
    mouseX,mouseY = event.x,event.y

    canPlace = True

    for y in range(0,len(elements)):
        e = elements[y]
        dx = e.x + e.width/2 - mouseX
        dy = e.y + e.height/2 - mouseY
        if math.sqrt(dx*dx+dy*dy) < 20:
            canPlace = False
            break

    if canPlace:
        elements.append(newElement(mouseX,mouseY))

mouseX = 0
mouseY = 0

w.bind("<B1-Motion>",mouse)

elements.append(newElement(300,50))
elements.append(newElement(300,100))

def collision(rect1,rect2):
   return rect1.x < rect2.x + rect2.width and rect1.x + rect1.width > rect2.x and rect1.y < rect2.y + rect2.height and rect1.height + rect1.y > rect2.y

def distance(rect1,rect2):
    dx = rect1.x - rect2.x
    dy = rect2.y - rect2.y
    return math.sqrt(dx*dx+dy*dy)

lastCall = time.time()

def engine():
    global lastCall
    w.delete("all")

    w.create_line(0,590,600,590)

    for i in range(0,len(elements)):
        e = elements[i]

        gravity = 0.1

        e.vy += gravity

        e.x += e.vx
        e.y += e.vy

        if e.y + e.height >= 590:
            e.y = 590-e.height
            e.vy = 0

        for x in range(0,len(elements)):
            e1 = elements[x]
            if e.id == e1.id or distance(e,e1) > 14.14:
                continue
            col = collision(e,e1)
            if col:
                e.y = e1.y - e1.height
                e.vy = 0

        w.create_rectangle(e.x,e.y,e.x+e.width,e.y+e.height,fill=e.color)

    w.create_text(10,10,anchor=NW,text=len(elements))
    fps = 60
    if time.time() - lastCall != 0:
        fps = round(1/(time.time() - lastCall))
    w.create_text(600,10,anchor=NE,text=fps)

    lastCall = time.time()

    master.after(16,engine)
engine()

mainloop()

Upvotes: 2

Views: 3030

Answers (2)

Bryan Oakley
Bryan Oakley

Reputation: 385870

Tkinter is capable of doing 60fps for a simple program like this, but performance will degrade depending on the algorithms you choose. I've outlined several things you can do to improve the performance of your code.

When I write a program similar to yours but with the changes outlined below, I'm able to get 60fps with over a thousand items.

Don't delete and redraw

The biggest problem is that you are deleting and creating the rectangles several several times a second. This is very inefficient. Compounding that, the canvas has performance issues once you have created many thousands of items, even if you later delete them. I've successfully animated many thousands of items, but if you create tens of thousands it can start to get sluggish.

What you should be doing instead is creating the items once, and then giving each item a move method so that you move the existing item rather than deleting and then recreating it.

Don't move items that don't need to be moved

The second problem is that you are moving all the objects in every frame, even though they have a zero velocity. There's no need to do the calculations for objects that won't move.

Let tkinter find the collisions for you

The third problem is that your algorithm for finding collisions is extremely inefficient. If you have 300 objects, you're doing 300x300 comparisons. For example, you'll check whether item 1 collides with item 2, then item 3, then item 4, etc. Then, you check whether item 2 collides with item 1, item 3, then item 4, etc. Since you've already determined whether item 1 and 2 have collided, there's no reason to see if items 2 and 1 have collided. Also, since the items are always moving straight down, you really only need to check collisions with items immediately below the current item.

You can solve the collision problem by letting tkinter do the work for you. Given an object, you can get a list of all objects that it overlaps with the find_overlapping method of the canvas. This is likely orders of magnitude faster than comparing every object to every object, since it's done internally by tkinter on the internal canvas data structures (ie: it's in C code rather than python code).

Upvotes: 7

scotty3785
scotty3785

Reputation: 7006

This doesn't solve your exact problem but will show you how to move objects rather than deleting and redrawing which will improve your performance. It also gives you an example of how you can perhaps make more use of classes and OOP to control the behavior of your object. This simple example will move the rectangle when the w,s,a and d keys are pressed.

#!/usr/bin/env python3
#Press w,s,a&d to move box
import tkinter as tk

class Box:
    def __init__(self,canvas,x1,y1,x2,y2,fill):
        self.canvas = canvas
        self.x1 = x1
        self.x2 = x2
        self.y1 = y1
        self.y2 = y2
        self.fill = fill
        self.objId = None
        self.drawBox()

    def drawBox(self):
        self.objId = self.canvas.create_rectangle(self.x1,self.y1,self.x2,self.y2,fill=self.fill)

    def moveBox(self,dx,dy):
        self.x1 += dx
        self.x2 += dx
        self.y1 += dy
        self.y2 += dy
        self.canvas.coords(self.objId,self.x1,self.y1,self.x2,self.y2)

def keypress(e):
    if e.char == 'a':
        box.moveBox(-2,0)
    elif e.char == 'd':
        box.moveBox(2,0)
    elif e.char == 'w':
        box.moveBox(0,-2)
    elif e.char == 's':
        box.moveBox(0,2)


root = tk.Tk()

w = tk.Canvas(root,width=200,height=200)
w.pack()

box = Box(w,50,100,160,180,'yellow')

root.bind('<KeyRelease>',keypress)


root.mainloop()

You can have an some extra code in your program that uses the after method to periodically update the position of the box (replicate gravity) and to iterate though a list of objects.

Upvotes: 1

Related Questions