如何让Tkinter按键事件循环与Canvas更新无延迟同步运行
我正在开发一款类似钢琴块的游戏,目前遇到游戏动画与事件循环无法同步运行的问题。希望分离检测KeyPress的事件循环与游戏动画逻辑,实现流畅运行,避免狂按按键时出现卡顿。想知道能否通过asyncio处理事件循环以避免动画卡顿?
尝试用asyncio方案时,游戏无法加载,出现如下错误:
DeprecationWarning: There is no current event loop loop = asyncio.get_event_loop()
见过不少将Tkinter与非Tkinter函数通过asyncio结合的方案,但未找到针对事件循环相关的解决办法。
相关代码
game.py
import threading import tkinter import main import time from playsound import playsound def open_file(music_file): """Open a music file.""" with open(music_file, 'r') as file: return file.read() class Game: def __init__(self, root): self.game_menu_sc = None self.place_to_hit = [] self.lines = [] self.number_of_keys = 12 self.items = None self.points = 0 self.root = root self.clickable_area = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9, 'k': 10, 'l': 11} def game_screen(self): """Create the game screen of the application.""" for widget in self.root.winfo_children(): widget.destroy() game_menu_sc = tkinter.Canvas(self.root) game_menu_sc.configure(bg='white') screen_width, screen_height = main.get_screen_size(self.root) width, height = main.scale_to_screen_size(screen_width, screen_height) game_menu_sc.bind('<KeyPress>', self.keyup) game_menu_sc.place(width=width, height=height) game_menu_sc.focus_set() for i in range(self.number_of_keys): line = game_menu_sc.create_line(i * int(width / self.number_of_keys), 0, i * int(width / self.number_of_keys), height, fill='black') self.lines.append(line) place_to_hit = game_menu_sc.create_rectangle(i * int(width / self.number_of_keys), int(height - 50), i * int(width / self.number_of_keys) + int( width / self.number_of_keys), int(height), fill='black') self.place_to_hit.append(place_to_hit) self.game_menu_sc = game_menu_sc self.animate_game(game_menu_sc, width) return True def animate_game(self, canvas, width): """Animate the game.""" self.items = self.play_music('music1.txt', width) while True: for item in self.items: canvas.move(item, 0, 1) canvas.update() time.sleep(0.01) if not self.items: print('points {}'.format(self.points)) break def keyup(self, event): """Handle keyup events.""" list_of_keycodes = [65, 83, 68, 70, 71, 72, 74, 75, 76, 186, 222, 13] for index, keycode in enumerate(list_of_keycodes): if keycode == event.keycode: threading.Thread(target=playsound, args=(f'assets/{index + 1 }.wav',), daemon=True).start() # winsound.PlaySound(f'assets/{index + 1}.wav', winsound.SND_FILENAME | winsound.SND_ASYNC) if self.get_collision(index): self.points += 1 print('collision') def get_collision(self, index): """Check if there is a collision and remove the object if there's a collision.""" p = self.game_menu_sc.coords(self.place_to_hit[index]) collision = self.game_menu_sc.find_overlapping(p[0], p[1], p[2], p[3]) collision = list(collision) collision_new = list(collision) for item in collision_new: if item in self.place_to_hit or item in self.lines: collision.remove(item) if len(collision) != 0: self.items.remove(collision[0]) self.game_menu_sc.delete(collision[0]) return True else: return False def play_music(self, music_file, width): """Create the rectangles based on the music file.""" music = open_file(music_file) notes = music.split(',') list_of_items = [] for note in notes: index, keys = note.split(':') for items in keys: i = self.clickable_area[items] rectangles = self.create_rectangle(i * int(width / self.number_of_keys) + 1, -float(index) * 50, i * int(width / self.number_of_keys) + int(width / self.number_of_keys) - 1, -float(index) * 50 + 50) list_of_items.append(rectangles) return list_of_items def create_rectangle(self, x1, y1, x2, y2): """Create rectangles.""" place_to_hit = self.game_menu_sc.create_rectangle(x1, y1, x2, y2, fill='black') return place_to_hit
music1.txt
1:a,2:b,3:ab,4:df,5:dl,6:ab,7:ah,8:hj,8:la,9:ga,10:la,11:fd
核心问题分析
卡顿的根源不是按键检测和动画的同步问题,而是animate_game方法里的while True + time.sleep阻塞了Tkinter的主事件循环。Tkinter所有的UI事件(包括按键、界面更新)都依赖主循环处理,一旦被阻塞,按键响应就会延迟,出现卡顿。
方案1:用Tkinter原生after方法(推荐,最简单)
替换animate_game里的死循环,用Tkinter的after方法实现非阻塞动画,让主事件循环始终能处理按键事件:
修改animate_game方法如下:
def animate_game(self, canvas, width): """Animate the game with non-blocking after method.""" self.items = self.play_music('music1.txt', width) def update_frame(): if self.items: for item in self.items: canvas.move(item, 0, 1) # 递归调用,每10ms更新一帧 canvas.after(10, update_frame) else: print('points {}'.format(self.points)) update_frame()
这个方案不需要额外引入asyncio,完全符合Tkinter的运行机制,能彻底解决卡顿问题。
方案2:结合asyncio(适合需要异步任务的场景)
如果一定要用asyncio,需要正确整合Tkinter主循环和asyncio事件循环,同时解决DeprecationWarning(Python 3.10+中get_event_loop()已被弃用,需显式创建或获取运行中的循环)。
步骤1:修改动画逻辑为异步函数
import asyncio async def async_animate(self, canvas): while self.items: for item in self.items: canvas.move(item, 0, 1) canvas.update() # 用asyncio.sleep替代time.sleep,不阻塞事件循环 await asyncio.sleep(0.01) print('points {}'.format(self.points))
步骤2:正确启动asyncio循环
在game_screen方法末尾,替换原有的self.animate_game调用,改为:
# 创建asyncio事件循环 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # 在asyncio循环中运行动画任务 loop.create_task(self.async_animate(game_menu_sc)) # 用Tkinter的after定期轮询asyncio循环,避免阻塞主循环 def poll_asyncio(): loop.run_until_complete(asyncio.sleep(0)) game_menu_sc.after(10, poll_asyncio) poll_asyncio()
步骤3:修复DeprecationWarning
原错误是因为没有当前运行的事件循环,改用asyncio.new_event_loop()显式创建,再用set_event_loop绑定,就不会触发警告。
内容的提问来源于stack exchange,提问作者Robert

