Python turtle-module: Onkeypress only available in loop when if-statement fullfilled

46 views Asked by At

I'm attempting to improve a 'Cross the Road'-style game (in scope of Angela Yu's Python-course) with certain player actions that are only executable a limited number of times.

The first I wanted to implement is 'Nitro', an instantaneous speed boost that's consumed on usage. The player starts with 0 nitro and receives one for every 2nd point scored.

At first the following code delivers the desired result, pressing space before receiving the first unit of nitro does nothing. Once nitro reaches 1 though the if-statement never closes again and the player can use nitro an arbitrary number of times. The remove.nitro-function works properly and removes one unit of nitro on each usage, resulting the built-in nitro-ticker to display negative numbers.

I experimented with putting a print-function inside the loop, it behaves like expected (only starts printing once nitro reaches 1 and stopping again when it drops lower).

Why do the onkeypress-functions behave differently?

while game_is_on:
screen.update()
time.sleep(.025)
screen.onkeypress(fun=player.move, key='Up')
if scoreboard.nitro >= 1:
    screen.onkeypress(fun=player.nitro, key='space')
    screen.onkeyrelease(fun=scoreboard.remove_nitro, key='space')
car_manager.drive()

Thanks in advance

1

There are 1 answers

4
ggorlen On

I'm not sure what your full game context looks like, but as Jason mentioned, key handlers should generally be registered once, before your game loop.

Furthermore, only the update loop should process the actions triggered by the currently pressed keys. The handlers are purely for tracking which keys are pressed at a given time. This ensures all the updates happen as a batch and avoids undesirable OS-specific key retrigger behavior.

Finally, the update function is where the game logic (basically, if statements) lives. You can compare the current time against your nitro state to grant and revoke nitros and adjust the player's speed accordingly and reposition all of your entities for the tick.

Here's a general proof of concept, a modification of this realtime turtle boilerplate, which you can adapt to your specific use case. For the sake of this example, nitros are given freely every 6 seconds, and double the player's speed for 4 seconds. The fundamental ideas are still the same regardless of how scoring and nitro-granting works in your application.

import turtle
from time import perf_counter

def tick():
    global last_nitro_given
    global last_nitro_activated
    global nitros
    t.clear()

    if perf_counter() - last_nitro_given > nitro_given_interval:
        last_nitro_given = perf_counter()
        nitros += 1

    nitro_active = perf_counter() - last_nitro_activated < nitro_duration
    if nitro_active:
        t.write(f"nitros: {nitros} [NITRO ACTIVE!]", font=("Arial", 16))
    else:
        t.write(f"nitros: {nitros}", font=("Arial", 16))

    for action in keys_pressed:
        actions[action]()

    turtle.update()
    win.ontimer(tick, frame_delay_ms)


def use_nitro():
    global nitros
    global last_nitro_activated

    nitro_active = perf_counter() - last_nitro_activated < nitro_duration
    if nitros > 0 and not nitro_active:
        nitros -= 1
        last_nitro_activated = perf_counter()


t = turtle.Turtle()
t.shapesize(2, 2, 2)
turtle.tracer(0)
frame_delay_ms = 1000 // 30
step_speed = 10
nitro_duration = 3
nitro_given_interval = 5
nitros = 0
last_nitro_given = perf_counter()
last_nitro_activated = 0

actions = dict(
    Left=lambda: t.left(step_speed),
    Right=lambda: t.right(step_speed),
    Up=lambda: t.forward(
        step_speed * 2
        if perf_counter() - last_nitro_activated < nitro_duration
        else step_speed
    ),
    space=use_nitro,
)
win = turtle.Screen()
keys_pressed = set()

def bind(key):
    win.onkeypress(lambda: keys_pressed.add(key), key)
    win.onkeyrelease(lambda: keys_pressed.remove(key), key)

for key in actions:
    bind(key)

win.listen()
tick()
win.exitonclick()

Note that global is poor design, only used here to keep the example reasonably scoped. In a larger app, use some sort of closure like a class or function to avoid globals (it seems you already have a class).