You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tkinter贪吃蛇游戏快速退出重进后蛇身重复绘制问题求助

问题根源

快速退出重进时蛇被多次绘制,核心原因是旧的游戏循环定时器未被取消。每次调用next_turn时,window.after(speed, next_turn, snake, food)会创建一个新的定时器,退出游戏时如果没取消这个定时器,它会继续执行,和新启动的游戏循环同时绘制蛇,导致多重蛇的问题。

解决步骤
  • 新增全局变量存储定时器ID,用于后续取消操作
  • 在退出游戏的函数(go_to_start_screen、game_over)中,取消未执行的定时器
  • 启动新游戏前,先清理可能残留的旧定时器
修改后的完整代码
from tkinter import *
import random

GAME_WIDTH = 700
GAME_HEIGHT = 700
SPACE_SIZE = 25
BODY_PARTS = 3
SNAKE_COLOUR = "#00A86B"
FOOD_COLOUR = "#DC143C"
BACKGROUND_COLOUR = "#2C3E50"
TITLE_FONT = ("Helvetica", 48, "bold")
BUTTON_FONT = ("Helvetica", 24)
TEXT_COLOR = "#ECF0F1"

speed = 100
mode_ = "Medium"
in_game = False  
in_start_screen = True  
after_id = None  # 新增:存储定时器ID

class Snake:
    def __init__(self):
        self.body_size = BODY_PARTS
        self.coordinates = []
        self.squares = []

        for i in range(BODY_PARTS):
            self.coordinates.append([0, 0])

        for x, y in self.coordinates:
            square = canvas.create_rectangle(x, y, x + SPACE_SIZE, y + SPACE_SIZE, fill=SNAKE_COLOUR, tag="snake")
            self.squares.append(square)

class Food:
    def __init__(self):
        x = random.randint(0, (GAME_WIDTH / SPACE_SIZE) - 1) * SPACE_SIZE
        y = random.randint(0, (GAME_HEIGHT / SPACE_SIZE) - 1) * SPACE_SIZE

        self.coordinates = [x, y]
        canvas.create_oval(x, y, x + SPACE_SIZE, y + SPACE_SIZE, fill=FOOD_COLOUR, tag="food")

def next_turn(snake, food):
    global direction, score, after_id

    x, y = snake.coordinates[0]

    if direction == "up":
        y -= SPACE_SIZE
    elif direction == "down":
        y += SPACE_SIZE
    elif direction == "left":
        x -= SPACE_SIZE
    elif direction == "right":
        x += SPACE_SIZE

    snake.coordinates.insert(0, (x, y))

    square = canvas.create_rectangle(x, y, x + SPACE_SIZE, y + SPACE_SIZE, fill=SNAKE_COLOUR)
    snake.squares.insert(0, square)

    if x == food.coordinates[0] and y == food.coordinates[1]:
        score += 1
        label.config(text=f"Score: {score}")

        canvas.delete("food")
        food = Food()

        if mode_ == "Dynamic":
            global speed
            speed = max(10, speed - 5)

    else:
        del snake.coordinates[-1]
        canvas.delete(snake.squares[-1])
        del snake.squares[-1]

    if check_collisions(snake):
        game_over()
    else:
        # 存储定时器ID
        after_id = window.after(speed, next_turn, snake, food)

def change_direction(new_direction):
    global direction

    if new_direction == "left" and direction != "right":
        direction = new_direction
    elif new_direction == "right" and direction != "left":
        direction = new_direction
    elif new_direction == "up" and direction != "down":
        direction = new_direction
    elif new_direction == "down" and direction != "up":
        direction = new_direction

def check_collisions(snake):
    x, y = snake.coordinates[0]

    if x < 0 or x >= GAME_WIDTH or y < 0 or y >= GAME_HEIGHT:
        return True

    for body_part in snake.coordinates[1:]:
        if x == body_part[0] and y == body_part[1]:
            return True

    return False

def game_over():
    global in_game, in_start_screen, after_id

    in_game = False
    in_start_screen = False  

    # 取消定时器
    if after_id:
        window.after_cancel(after_id)
        after_id = None

    canvas.delete(ALL)
    canvas.create_text(canvas.winfo_width() / 2, canvas.winfo_height() / 2,
                       font=("Helvetica", 50, "bold"), text="GAME OVER", fill=FOOD_COLOUR, tag="gameover")
    canvas.create_text(canvas.winfo_width() / 2, canvas.winfo_height() / 1.5,
                       font=("Helvetica", 20), text="\nPress SPACE to try again.\nPress ESC to return to the home screen.", fill=FOOD_COLOUR, tag="gameover")
    window.bind("<space>", lambda event: start_game())
    window.bind("<Escape>", lambda event: go_to_start_screen())

def start_game():
    global score, direction, food, snake, label, canvas, in_game, in_start_screen, after_id

    in_game = True
    in_start_screen = False  

    # 先取消可能存在的旧定时器
    if after_id:
        window.after_cancel(after_id)
        after_id = None

    canvas.delete("snake")
    canvas.delete("food")
    canvas.pack_forget() 
    if label:
        label.destroy()
    if start_button:
        start_button.destroy()
    if title_label:
        title_label.destroy()
    if easy_button:
        easy_button.destroy()
    if medium_button:
        medium_button.destroy()
    if hard_button:
        hard_button.destroy()
    if dynamic_button:
        dynamic_button.destroy()

    canvas.pack(side=BOTTOM, anchor="n")  

    canvas.delete(ALL)
    label = Label(window, text="Score: 0", font=("Helvetica", 40), bg=BACKGROUND_COLOUR, fg=TEXT_COLOR, width=23)
    label.pack(pady=10)
    score = 0
    direction = "down"
    label.config(text=f"Score: {score}")

    snake = Snake()
    food = Food()
    next_turn(snake, food)

    window.bind("<Escape>", lambda event: go_to_start_screen())

def start_screen():
    global start_button, title_label, easy_button, medium_button, hard_button, canvas, window, button_style, in_game, in_start_screen, dynamic_button

    in_game = False
    in_start_screen = True

    canvas.pack_forget()  
    canvas.delete(ALL)

    window.geometry(f"{GAME_WIDTH}x{GAME_HEIGHT + 100}") 
    window.configure(bg=BACKGROUND_COLOUR)  

    title_label = Label(window, text="Snake Game", font=TITLE_FONT, bg=BACKGROUND_COLOUR, fg=TEXT_COLOR)
    title_label.pack(pady=(40, 20)) 

    button_style = {
        'font': ("Helvetica", 20),  
        'bg': "#2980B9",  
        'fg': TEXT_COLOR,
        'activebackground': "#3498DB",  
        'activeforeground': TEXT_COLOR,
        'bd': 0, 
        'width': 18,
        'height': 2,
        'cursor': 'hand2'  
    }

    start_button = Button(window, text=f"Start Game\n({mode_})", **button_style, command=start_game)
    easy_button = Button(window, text="Easy", **button_style, command=lambda: update_mode("easy"))
    medium_button = Button(window, text="Medium", **button_style, command=lambda: update_mode("medium"))
    hard_button = Button(window, text="Hard", **button_style, command=lambda: update_mode("hard"))
    dynamic_button = Button(window, text="Dynamic", **button_style, command=lambda: update_mode("dynamic"))

    easy_button.pack(pady=10)
    medium_button.pack(pady=10)
    hard_button.pack(pady=10)
    dynamic_button.pack(pady=10)
    start_button.pack(pady=(20, 10))

    window.unbind("<Escape>")

def update_mode(selected_mode):
    global speed, mode_, start_button

    if selected_mode == "easy":
        speed = 100
        mode_ = "Easy"
    elif selected_mode == "medium":
        speed = 75
        mode_ = "Medium"
    elif selected_mode == "hard":
        speed = 50
        mode_ = "Hard"
    elif selected_mode == "dynamic":
        speed = 100
        mode_ = "Dynamic"

    start_button.config(text=f"Start Game\n({mode_})")

def go_to_start_screen():
    global canvas, in_game, in_start_screen, after_id

    if in_game or not in_start_screen:
        # 取消定时器
        if after_id:
            window.after_cancel(after_id)
            after_id = None

        canvas.pack_forget()
        canvas.delete(ALL)
        canvas.delete("snake")
        canvas.delete("food")
        label.destroy()

        start_screen()

window = Tk()
window.title("Snake Game")

label = Label(window, text="", bg=BACKGROUND_COLOUR)
label.pack()

canvas = Canvas(window, bg=BACKGROUND_COLOUR, height=GAME_HEIGHT, width=GAME_WIDTH)

window.bind("<Left>", lambda event: change_direction("left"))
window.bind("<Right>", lambda event: change_direction("right"))
window.bind("<Up>", lambda event: change_direction("up"))
window.bind("<Down>", lambda event: change_direction("down"))
window.bind("<a>", lambda event: change_direction("left"))
window.bind("<d>", lambda event: change_direction("right"))
window.bind("<w>", lambda event: change_direction("up"))
window.bind("<s>", lambda event: change_direction("down"))

start_screen()

window.mainloop()
关键修改点说明
  • 新增全局变量after_id,保存window.after()返回的定时器ID
  • 在next_turn中,将定时器ID赋值给after_id
  • 在game_over、go_to_start_screen、start_game函数中,调用window.after_cancel(after_id)取消未执行的定时器,确保旧游戏循环完全停止

内容的提问来源于stack exchange,提问作者user27164932

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 00:58:14