Python Forum
[Tkinter] Tkinter/Turtle Stopping and Starting Timers Cleanly
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Tkinter/Turtle Stopping and Starting Timers Cleanly
#1
I have come across several situations recently writing programs using Python Turtle Graphics where the ontimer() turtle method has been insufficient for my needs and I've had to use the underlying tkinter methods after() and after_cancel().

Even with this power at my hands, I've had real trouble implementing start/stop functionality on timers. It seems that every time I start a new timer, it somehow combines with an existing one and the callbacks go haywire.

I finally found a solution as shown in the code below. As it is, dropping more than one bomb results in an uncontrolled speeding up of the bomb timer. After a great deal of research and experiment, I came up with the solution shown in the comment in the code in __continue_bomb_drop(), which is to cancel the existing timer within __continue_bomb_drop(), even though though it looks like it should have been killed by after_cancel() in stop_bomb_drop().

From what I've read, rather than this seemingly complex solution, I would have thought its was enough to unbind the keyboard event handler during the execution of the bomb drop, to avoid event stacking, but apparently not.

Can someone please explain why I need to after_cancel() the bomb_timer_id in both __continue_bomb_drop() and
stop_bomb_drop()? Also, please tell me if there is a simpler/more canonical way to achieve what I am currently doing with the three functions start_bomb_drop(), __continue_bomb_drop() and stop_bomb_drop()?

import turtle
import random


CURSOR_SIZE = 20
PLANE_DELAY = 10 # maybe increase speed as we go.... 10
BOMB_DELAY = 50

def move_plane():
    new_pos = (plane.xcor(), plane.ycor())
    if new_pos[0] > width // 2:
        plane.goto(- width // 2, plane.ycor() - size)
    else:
        plane.goto(plane.xcor() + 12, plane.ycor())
    if tower_collision(plane, towers):
        plane.goto( - width // 2 , height // 2)        
    screen.update()
    turtle.ontimer(move_plane, PLANE_DELAY)
    
    
def cell_collision(tur1, tur2):
    if tur1.distance(tur2) <= size / 2:
        return True
        
def tower_collision(tur, towers):
    for tower in towers:
        for cell in tower:
            if cell_collision(tur, cell):
                return True
                
def bomb_collision():
    # Should I use global towers or pass as argument?
    pass
 
 
def start_bomb_drop():
    global bomb_dropping
    screen.onkey(None, "space")
    bomb.goto(plane.xcor(), plane.ycor())
    bomb.showturtle()
    __continue_bomb_drop()
    
    
def __continue_bomb_drop():
    global bomb_dropping, bomb_timer_id
    try:
        pass
        # canvas.after_cancel(bomb_timer_id) # Uncomment and remove "pass" to fix bug.
    except NameError:
        pass
    bomb.goto(bomb.xcor(), bomb.ycor() - 12)
    if bomb.ycor() < - height // 2 or bomb_collision():
        stop_bomb_drop()
    bomb_timer_id  = canvas.after(BOMB_DELAY, __continue_bomb_drop)
    
def stop_bomb_drop():
    global bomb_dropping, bomb_timer_id
    bomb.hideturtle()
    screen.onkey(start_bomb_drop, "space")
    canvas.after_cancel(bomb_timer_id)
        
# Screen   
screen = turtle.Screen()
canvas = screen.getcanvas()
screen.title("Alien Bomber")
screen.setup(800, 600)
screen.bgcolor("dark blue")
screen.listen()
screen.onkey(start_bomb_drop, "space")
screen.tracer(0)

# MISC.
cells = 20
cell_colors = ["black", "dark green", "brown"]
width = screen.window_width() - 50
height = screen.window_height() - 50
size = width / cells
offset = (cells % 2) * size/2 + size/2  # Center even and odd cells

# Build towers
towers = []
for col in range(-cells // 2, cells // 2):
    tower = []
    for level in range(random.randrange(1, 11)):
        block = turtle.Turtle(shape="square")
        block.shapesize(size / CURSOR_SIZE)
        block.color(random.choice(cell_colors))
        block.penup()
        block.goto(col * size + offset, - height // 2 + level * size + offset)
        tower.append(block)
    towers.append(tower)
    
# Plane
plane = turtle.Turtle(shape="triangle", visible=False)
plane.color("yellow")
plane.shapesize(20 / CURSOR_SIZE, 40 / CURSOR_SIZE)
plane.penup()
plane.goto( - width // 2 , height // 2)
plane.showturtle()

# Bomb
bomb = turtle.Turtle(shape="circle")
bomb.color("red")
bomb.shapesize(0.5)
bomb.penup()
bomb.hideturtle()
bomb_dropping = False

# Score
score = 0
high_score = 0
pen = turtle.Turtle()
# pen.shape("square")
pen.color("white")
pen.penup()
pen.hideturtle()
pen.goto(0, 260)
pen.write("Score: 0 High Score: {}".format(high_score), align="center", font=("Courier", 24, "normal"))

# Begin
screen.update()
move_plane()
turtle.done()
Reply
#2
I think the problem is clear if you put the stop_bomb_drop cod inside the __continue_bomb_drop func.
def __continue_bomb_drop():
    global bomb_dropping, bomb_timer_id
    bomb.goto(bomb.xcor(), bomb.ycor() - 12)
    if bomb.ycor() < - height // 2 or bomb_collision():
        bomb.hideturtle()
        screen.onkey(start_bomb_drop, "space")
        canvas.after_cancel(bomb_timer_id)
    bomb_timer_id  = canvas.after(BOMB_DELAY, __continue_bomb_drop)
Notice that even if you stop dropping the bomb you continue executing the __continue_bomb_drop. See what happens if you use an else statement so either you stop the bomb or you continue, not both.
def __continue_bomb_drop():
    global bomb_dropping, bomb_timer_id
    bomb.goto(bomb.xcor(), bomb.ycor() - 12)
    if bomb.ycor() < - height // 2 or bomb_collision():
        bomb.hideturtle()
        screen.onkey(start_bomb_drop, "space")
        canvas.after_cancel(bomb_timer_id)
    else:
        bomb_timer_id  = canvas.after(BOMB_DELAY, __continue_bomb_drop)
The second and subsequent bombs dropped faster because you are calling the continue twice as often. At first I was really confused by why each bomb didn't drop faster than the previous, but then I realized the way you stopped and started the bombs prevented more than two "timer loops" from forming. If you had don't this on purpose it would be a really clever bit of programming (hopefully with a lot of comments).
Reply
#3
Hey thanks @deanhystad that was super-useful. I've been pulling my hair out over this for a while now!

@deanhystad I've just noticed that with your insight, I can now go back to using plain old Turtle methods:

def start_bomb_drop():
    # Prevent further key presses until drop is finished tp prevent event stacking.
    screen.onkey(None, "space")  #
    bomb.goto(plane.xcor(), plane.ycor())
    bomb.showturtle()
    __continue_bomb_drop()
    
    
def __continue_bomb_drop():
    bomb.goto(bomb.xcor(), bomb.ycor() - 12)
    if bomb.ycor() < - height // 2 or bomb_collision():
        stop_bomb_drop()
    else:
        turtle.ontimer(__continue_bomb_drop, BOMB_DELAY)
    
    
def stop_bomb_drop():
    bomb.hideturtle()
    # It's now safe to allow another bomb drop, so rebind keyboard event.
    screen.onkey(start_bomb_drop, "space")
Big Grin
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Turtle / turtle question DPaul 2 516 Oct-04-2020, 09:23 AM
Last Post: DPaul
  [Tkinter] Tkinter/turtle doevents DPaul 0 556 Sep-24-2020, 07:59 AM
Last Post: DPaul
  tkinter window and turtle window error 1885 3 2,797 Nov-02-2019, 12:18 PM
Last Post: 1885
  [Tkinter] Is there a way to sleep without stopping user input? GalaxyCoyote 2 796 Oct-23-2019, 06:23 PM
Last Post: Denni

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020