Started playing with python's tkinter today and ran into some problems.
I created an animation that moves a ball around the screen, with a given speed. (and when it hits the screen, it goes back)
Why does my ball look bad? it's shape is not uniform? (its like blinking a lot)
Is there a better way to do it?
the code:
from tkinter import *
import time
WIDTH = 800
HEIGHT = 500
SIZE = 100
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, bg="grey")
canvas.pack()
color = 'black'
class Ball:
def __init__(self):
self.shape = canvas.create_oval(0, 0, SIZE, SIZE, fill=color)
self.speedx = 3
self.speedy = 3
def update(self):
canvas.move(self.shape, self.speedx, self.speedy)
pos = canvas.coords(self.shape)
if pos[2] >= WIDTH or pos[0] <= 0:
self.speedx *= -1
if pos[3] >= HEIGHT or pos[1] <= 0:
self.speedy *= -1
ball = Ball()
while True:
ball.update()
tk.update()
time.sleep(0.01)
errors after terminating the program:
Traceback (most recent call last):
File "C:/..py", line 29, in <module>
ball.update()
File "C:/Users/talsh/...py", line 20, in update
canvas.move(self.shape, self.speedx, self.speedy)
File "C:\Users\...\tkinter\__init__.py", line 2585, in move
self.tk.call((self._w, 'move') + args)
_tkinter.TclError: invalid command name ".!canvas"
Is it normal? Am I doing anything wrong?
I would imaging the problem is coming from sleep(). The methods sleep() and wait() should not be used in tkinter as they will pause the entire application instead of just providing a timer.
Update:
Its also not a good idea to name a method the same name as a built in method.
you have self.update() and update() is already in the name space for canvas. Change self.update() to something else like: self.ball_update()
UPDATE:
It looks like tikinter refreshes at a 15ms rate and trying to fire an even faster than that might cause issues. The closest I was able to get to stopping the circle from distorting while moving at the same rate as your original code was to change the timer to 30ms and to change your speed variables to 9 from 3.
Always make sure you have mainloop() at the end of you tkinter app. mainloop() is required to make sure tkinter runs properly and without there may be bugs caused by it missing so at the end add tk.mainloop()
You should use after() instead. This should probably be done using a function/method as your timed loop. Something like this:
def move_active(self):
if self.active == True:
self.ball_update()
tk.after(30, self.move_active)
tk.update()
Replace your while loop with the above method and add the class attribute self.active = True to your __init__ section. Let me know if this clears up your stuttering:
from tkinter import *
import time
WIDTH = 800
HEIGHT = 500
SIZE = 100
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, bg="grey")
canvas.pack()
color = 'black'
class Ball:
def __init__(self):
self.shape = canvas.create_oval(0, 0, SIZE, SIZE, fill=color)
self.speedx = 9 # changed from 3 to 9
self.speedy = 9 # changed from 3 to 9
self.active = True
self.move_active()
def ball_update(self):
canvas.move(self.shape, self.speedx, self.speedy)
pos = canvas.coords(self.shape)
if pos[2] >= WIDTH or pos[0] <= 0:
self.speedx *= -1
if pos[3] >= HEIGHT or pos[1] <= 0:
self.speedy *= -1
def move_active(self):
if self.active == True:
self.ball_update()
tk.after(30, self.move_active) # changed from 10ms to 30ms
ball = Ball()
tk.mainloop() # there should always be a mainloop statement in tkinter apps.
Here are some links to Q/A's related to refresh timers.
Why are .NET timers limited to 15 ms resolution?
Why does this shape in Tkinter update slowly?
All that being said you may want to use an alternative that might be able to operate at a faster refreash rate like Pygame
UPDATE:
Here is an image of what is happening to the circle while its moving through the canvas. As you can see its getting potions of the circle visibly cut off. This appears to happen the faster the update is set. The slower the update( mostly above 15ms) seams to reduce this problem:
After suffering the same flattened leading edges of fast moving objects I am inclined to agree with #Fheuef's response, although I solved the problem in a different manner.
I was able to eliminate this effect by forcing a redraw of the entire canvas just by resetting the background on every update.
Try adding:
canvas.configure(bg="grey")
to your loop. Of course we compromise performance, but it's a simple change and it seems a reasonable trade off here.
Basically I've found that this has to do with the way Tkinter updates the canvas image : instead of redrawing the whole canvas everytime, it forms a box around things that have moved and it redraws that box. The thing is, it seems to use the ball's old position (before it moved) so if the ball moves too fast, its new position is out of the redraw box.
One simple way to solve this however is to create a larger invisible ball with outline='' around it, which will move to the ball's position on every update, so that the redraw box takes that ball into account and the smaller one stays inside of it. Hope that's clear enough...
Related
I'm creating a simple game with Python using turtle package.
My aim is to have some balloons on the screen running from right to left and then when one of them is clicked to make it disappear.
What I have going wrong is that when I click one balloon, all of them are disappeared!
Here is my code
1- Main
from turtle import Screen
from balloon import Balloon
import time
screen = Screen()
screen.title('Balloons Nightmare')
screen.setup(width=600, height=600)
screen.bgpic(picname='sky-clouds.gif')
screen.tracer(0)
balloons_manager = Balloon()
current_x = 0
current_y = 0
screen.listen()
screen.onclick(fun=balloons_manager.explode_balloon, btn=1)
game_is_on = True
while game_is_on:
time.sleep(0.1)
screen.update()
balloons_manager.create_balloon()
balloons_manager.move_balloon()
screen.exitonclick()
2- balloon module
import random
from turtle import Turtle
COLORS = ["red", "yellow", "green", "blue", "black"]
MOVEMENT_SPEED = 2
class Balloon:
def __init__(self):
self.all_balloons = []
self.balloon_speed = MOVEMENT_SPEED
self.x = 0
self.y = 0
self.hidden = None
def create_balloon(self):
random_choice = random.randint(1, 9)
if random_choice == 1:
new_balloon = Turtle("circle")
new_balloon.penup()
new_balloon.turtlesize(stretch_wid=2, stretch_len=0.75)
new_balloon.color(random.choice(COLORS))
random_y_cor = random.randint(-50, 280)
new_balloon.goto(320, random_y_cor)
self.hidden = new_balloon.isvisible()
self.all_balloons.append(new_balloon)
def move_balloon(self):
for balloon in self.all_balloons:
balloon.backward(self.balloon_speed)
def explode_balloon(self, x, y):
for balloon in range(len(self.all_balloons)):
print(self.all_balloons[balloon].position())
self.all_balloons[balloon].hideturtle()
# for balloon in self.all_balloons:
# balloon.hideturtle()
I tried so many changes but nothing helped me, example of what I tried so far
getting the current x,y coordinates of the balloon so that on click to hide only the one with these coordinates but didn't work for me or I did something wrong with it
Any hints will be appreciated.
Thank you
I fixed the issue by adding an inner function in create_balloon function
def create_balloon(self):
random_choice = random.randint(1, 9)
if random_choice == 1:
new_balloon = Turtle("circle")
new_balloon.penup()
new_balloon.turtlesize(stretch_wid=2, stretch_len=0.75)
new_balloon.color(random.choice(COLORS))
random_y_cor = random.randint(-50, 280)
new_balloon.goto(320, random_y_cor)
def hide_the_balloon(x, y):
return new_balloon.hideturtle()
new_balloon.onclick(hide_the_balloon)
self.all_balloons.append(new_balloon)
Here's your code triggered by the click handler:
def explode_balloon(self, x, y):
for balloon in range(len(self.all_balloons)):
print(self.all_balloons[balloon].position())
self.all_balloons[balloon].hideturtle()
This loops over all balloons and hides them unconditionally.
You probably want to use an if in there to only conditionally trigger the hiding behavior. Compare the x and y coordinates of the click against the current balloon in the loop. Only hide the balloon if the distance is less than a certain amount (say, the radius of the balloon).
Another approach is to use turtle.onclick to add a handler function that will be trigged when the turtle is clicked.
Related:
How to see if a mouse click is on a turtle in python
Python find closest turtle via mouse click
Why is my Python Turtle program slowing down drastically the longer it runs? (since you're never removing turtles and constantly adding new ones, this is a good thread to take a look at)
I'm trying to make a scroll bar and at the moment, the scroll works by changing the coordinates when blitting (as opposed to changing the actual rect coordinates). This means that rect collisions for buttons do not work when they are moved. I am attempting to combat this by calculating the percentage that the scroll bar has scrolled, converting that to some multiplier or screen coordinate, and then getting the mouse position.
Some notes:
Self.bar is the actual slider handle (the small thing you use to scroll)
Self.rect is the entire slider, and its height is equal to screen height
Self.total_h is the total height that the scroll bar needs to scroll, for example if it needed to scroll to 2x the screen height then total_h would equal screen_height * 2.
Some code I have tried so far:
# Calculate the distance between the top of the handle and the top of the overall bar and divide by the handle height
# (shortened from ((self.bar.rect.top - self.rect.top) / self.rect.h) * (self.rect.h / self.bar.rect.h) which makes more intuitive sense.
self.scroll_percent = ((self.bar.rect.top - self.rect.top) / self.bar.rect.h)
# These all do not work:
# pos_y = pg.mouse.get_pos()[1] * self.scroll_percent
# pos_y = pg.mouse.get_pos()[1] * (self.total_h / self.scroll_percent)
# pos_y = (self.total_h / self.scroll_percent) * pg.mouse.get_pos()[1]
# etc
The logic just doesn't make sense to me, and I've got no idea how to do this. To clarify, my goal is to allow the user to scroll the screen using a scroll bar, and depending on the scroll bar's position, we change the mouse pos accordingly.
I don't really understand why you bother with some percentage ? If I understood correctly you are only scrolling up and down so the only thing you need to know is the y offset, which is 0 when the scroll bar is at the top and then it is just the y value at which you are blitting your surface. So simply remove the y offset to your mouse y when you check for collision.
Maybe I missed something ?
If I understood corretly, here is an simple example of what to do :
(I didn't recreate the scroll bar since you said you've got this part working. I just made the surface go up automatically. I'm sure you will figure out a way to integrate this solution to your own code)
# General import
import pygame as pg
import sys
# Init
pg.init()
# Display
screen = pg.display.set_mode((500, 500))
FPS = 30
clock = pg.time.Clock()
# Surface declaration
drawing_surface = pg.Surface((screen.get_width(), screen.get_height() * 2))
drawing_surface.fill((255,0,0))
drawing_surface_y = 0
# Button
test_btn = pg.Rect(20, 400, 100, 40)
# Main functions
def update():
global drawing_surface_y
drawing_surface_y -= 1
def draw():
# Clear the screen
screen.fill((0,0,0))
# Render the button
pg.draw.rect(drawing_surface, (0,0,255), test_btn)
# Blit the drawing surface
screen.blit(drawing_surface, (0, drawing_surface_y))
def handle_input():
for evt in pg.event.get():
if evt.type == pg.QUIT:
exit()
if evt.type == pg.MOUSEBUTTONDOWN:
if evt.button == 1:
on_click()
def on_click():
mx, my = pg.mouse.get_pos()
if test_btn.collidepoint(mx, my - drawing_surface_y):
print("Test button has been clicked")
def exit():
pg.quit()
sys.exit()
# Other functions
# Main loop
if __name__ == "__main__":
while True:
handle_input()
update()
draw()
pg.display.update()
clock.tick(FPS)
Test this code and let me know if it answers your question !
I have an image of a map. I would like to make the left and right (East and west) edges of the map connect so that you can scroll forever to the right or left and keep scrolling over the same picture. I've looked around and can't find anything on the topic (likely because I don't know what to call it). I would also like to have the picture in a frame that I can grab and drag to move the picture around. I was trying to do this in Tkinter, but I have a feeling there are probably easier ways to do this.
(actually, you are asking 2 different, not very precise questions)
scroll forever: Independent from python a common approach is to
mirror the images at the edges so you can implement a virtually
endless world from 1 or some images (tiles of the map).
GUI framework/API: From my experience Qt (so in your case maybe PyQt) is
well documented and designed to quite easily realize OS independent
GUI.
I was able to get the results I wanted with pygame.
blit(source, dest, area=None, special_flags = 0) -> Rect
I setup a rectangle twice the width of my map, and set up a function to always have two maps drawn side by side. I added functions to move the map as a % of the tile width.
SCREENRECT = Rect(0, 0, 6025, 3010)
...
class Arena:
speed = 15
def __init__(self):
w = SCREENRECT.width
h = SCREENRECT.height
self.tilewidth = self.oceantile.get_width()
self.tileheight = self.oceantile.get_height()
print self.tilewidth, self.tileheight
self.counter = 0
self.counter2 = 0
self.ocean = pygame.Surface((w+self.tilewidth,h)).convert()
for x in range(w/self.tilewidth):
for y in range(h/self.tileheight):
self.ocean.blit(self.oceantile, (x*self.tilewidth, y*self.tileheight))
def left(self):
self.counter = (self.counter - self.speed) % self.tilewidth
def right(self):
self.counter = (self.counter + self.speed) % self.tilewidth
def up(self):
if self.counter2 > 0: self.counter2 = (self.counter2 - self.speed) % self.tileheight
def down(self):
if self.counter2 < 1140: self.counter2 = (self.counter2 + self.speed) % self.tileheight
screen.blit(arena.map, (0, 0), (arena.counter, arena.counter2, SCREENRECT.width, SCREENRECT.height))
I then used the blit function to draw the map, with x and y pixels shaved off via the area input.
blit(source, dest, area=None, special_flags = 0) -> Rect
screen.blit(arena.map, (0, 0), (arena.counter, arena.counter2, SCREENRECT.width, SCREENRECT.height)).
Currently I control the scrolling of the mouse with the keyboard, but the grab and drag functionality shouldn't be to hard to figure out with the pygame modules.
First of all, take a look at my previous thread here: Tkinter understanding mainloop
After following the advice from there, in GUI programming, infinite loops have to be avoided at all costs, in order to keep the widgets responsive to user input.
Instead of using:
while 1:
ball.draw()
root.update()
time.sleep(0.01)
I managed using self.canvas.after(1, self.draw) inside my draw() function.
So my code now looks like this:
# Testing skills in game programming
from Tkinter import *
root = Tk()
root.title("Python game testing")
root.resizable(0, 0)
root.wm_attributes("-topmost", 1)
canvas = Canvas(root, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
root.update()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
self.canvas_height = canvas.winfo_height()
self.x = 0
self.y = -1
def draw(self):
self.canvas.move(self.id, self.x, self.y)
pos = self.canvas.coords(self.id)
if pos[1] <= 0:
self.y = 1
if pos[3] >= self.canvas_height:
self.y = -1
self.canvas.after(2, self.draw)
ball = Ball(canvas, "red")
ball.draw()
root.mainloop()
However, time inside self.canvas.after() does not work properly...
If set to 1 it's extremely fast! If it's set to 10, 5 or even 2, it's too slow!
I didn't have that problem while using the above while loop in my code since the time.sleep() worked as it should!
EDIT:
I can now report that the time inside the after function of Tkinter does not work properly in my Windows 8.1 tablet, and in my Windows 8.1 laptop, while in the same laptop when running Ubuntu through virtual machine it does work as it should.
Time in time.sleep is in seconds whereas in after() it is in milliseconds. time.sleep(0.01) is the same as self.canvas.after(10, self.draw). If after(2, func) was too slow but after(1, func) was too fast then you could try sleep(0.0005) then after(1, func) do give a delay of 1.5 milliseconds but it is barely noticeable. Either way, fiddle with the timing until the pause is right.
Speed of the object (canvas) and clock/loop speed should be considered as two different things, IMHO.
Hence, you can leave loop after 2ms or even larger like 10...25...
...
self.canvas.after(2, self.draw)
... # loop this after 2 ms.
while, changing speed in terms of 'how many pixels' :
pos = self.canvas.coords(self.id)
if pos[1] <= 0:
self.y = 20
if pos[3] >= self.canvas_height:
self.y = -20
so adjusting these values, and :
self.canvas.move(self.id, 245, 100)
can let you fine-tune position and speed of your red dot.
Hope it helps
Both items will move fine however, whenever I am using the bound keys to the event handler the other image (enemy) will disappear until i stop moving the player with the keys. Is there a way around this? I've tried the canvas method .move also and tried searching didn't find a solution from the searches I tried.
def move_player(self, event):
if event.keysym.lower() == "a":
self.player_x -= self.player_delta_x
if event.keysym.lower() == "d":
self.player_x += self.player_delta_x
self.canvas.delete(self.player)
self.make_player()
def move_enemy(self):
self.enemy_x += self.enemy_delta_x
self.enemy_y -= self.enemy_delta_y
#Keep the enemy within the bounds of the screen
if self.enemy_x > self.width - self.enemy_radius:
self.enemy_delta_x *= -1
if self.enemy_x < 0:
self.enemy_delta_x *= -1
if (self.enemy_y >= height / 2 or
self.enemy_y <= 0 + self.enemy_radius):
self.enemy_delta_y *= -1
self.canvas.delete(self.enemy)
self.make_enemy()
if __name__ == "__main__":
root = tk.Tk()
canvas = tk.Canvas(root, bg = "black")
width = root.winfo_screenwidth()
height = root.winfo_screenheight() - TASKBAR_OFFSET
root.geometry("%dx%d" % (width, height))
game = Game(root, canvas, width, height)
game.make_menu()
game.make_background()
game.make_player()
game.make_enemy()
while True:
game.move_enemy()
game.canvas.update()
time.sleep(0.025)
root.mainloop()
First of all, instead of using a while True loop, use root.after(0, animateFunction).
And then inside of the animate function:
def animateFunction():
#Movement Code
moveEnemyFunction()
checkPlayerMovement()
root.after(animateFunction, 10)
If you loop like this, it should complete the movement task each frame and ultimately move the player without losing the enemy each frame the player moves.
Now, there is no guarantee this will work, because I have no idea how your movement works and how you bound it, but I believe this should solve your problem.