Pygame游戏窗口点击后无响应问题求助
Pygame客户端点击窗口无响应问题修复
问题背景
用Pygame开发连接服务器的游戏客户端,服务器返回玩家、怪物等实体列表,窗口负责绘制并更新状态。但点击窗口后会停止响应,尝试模拟实体、关闭相关线程都无法解决,怀疑主循环存在问题。
核心问题分析
- Pygame线程安全限制:Pygame的图形绘制API(如
blit、display.update)和事件处理必须在主线程执行,原代码将draw_loop放在子线程运行,会导致图形上下文混乱,引发窗口无响应。 - 主循环无帧率控制:原主循环未添加延迟,会疯狂占用CPU资源,导致系统无法及时处理窗口事件(如点击、关闭)。
- 事件处理错误:
event.type == pygame.quit拼写错误,应为pygame.QUIT;且未导入sys模块,调用sys.exit()会触发报错。 - 共享资源无锁保护:子线程
entity_loop修改self.entities时,主线程可能同时读取该列表,引发数据竞争,导致绘制异常或卡死。
修复方案
- 将绘制逻辑整合到主线程主循环中,取消独立的
draw_thread。 - 给主循环添加帧率控制,避免CPU占用过高。
- 修正事件处理错误,导入缺失的
sys模块。 - 为共享的
self.entities列表添加线程锁,确保读写安全。 - 将子线程设置为守护线程,主线程退出时自动终止子线程。
修复后完整代码
import pygame import requests import threading import time import sys from emptyjsonerror import EmptyJSONError server_url = "localhost:5000" username = None class GameClient: """连接服务器并使用Pygame显示游戏状态的客户端类""" def __init__(self, server_url, screen_size, tile_size, color): self.server_url = server_url self.screen_size = screen_size self.tile_size = tile_size self.color = color pygame.init() self.screen = pygame.display.set_mode(screen_size) pygame.display.set_caption("Simple Client") self.entities = [] self.images = {} self.lock = threading.Lock() def get_tile_map(self): """从服务器获取 tile 地图数据""" response = requests.get(self.server_url + "/map") if not response.content or response.headers.get("Content-Type") != "application/json": raise EmptyJSONError("Empty or invalid JSON response from server", response.url) return response.json() def draw_tile_map(self, tile_map): """绘制 tile 地图""" for tile in tile_map: x = tile["x"] y = tile["y"] pixel_x = x * self.tile_size[0] pixel_y = y * self.tile_size[1] rect = pygame.Rect(pixel_x, pixel_y, self.tile_size[0], self.tile_size[1]) pygame.draw.rect(self.screen, self.color, rect) def get_entities(self, debug=False): """从服务器获取并更新实体列表""" if debug: dummy_entity = { "type": "Player", "id": 0, "x": 5, "y": 5, "level": 1, "health": 100 } with self.lock: existing_entity = next((e for e in self.entities if e.get("type") == dummy_entity.get("type") and e.get("id") == dummy_entity.get("id")), None) if not existing_entity: self.entities.append(dummy_entity) else: response = requests.get(self.server_url + "/entities", params={"username": username}) print(f"Entities: {response.json()}") if not response.content or response.headers.get("Content-Type") != "application/json": raise EmptyJSONError("Empty or invalid JSON response from server", response.url) new_entities = response.json() with self.lock: if not self.entities: self.entities = new_entities else: # 添加或更新新实体 for new_entity in new_entities: entity_type = new_entity.get("type") entity_id = new_entity.get("id") old_entity = next((e for e in self.entities if e.get("type") == entity_type and e.get("id") == entity_id), None) if not old_entity: self.entities.append(new_entity) else: old_entity.update(new_entity) # 移除已消失的实体 entities_to_remove = [] for old_entity in self.entities: entity_type = old_entity.get("type") entity_id = old_entity.get("id") new_entity = next((e for e in new_entities if e.get("type") == entity_type and e.get("id") == entity_id), None) if not new_entity: entities_to_remove.append(old_entity) for entity in entities_to_remove: self.entities.remove(entity) time.sleep(1/60) def draw_entities(self): """绘制所有实体""" with self.lock: for entity in self.entities: entity_type = entity.get("type") x = entity.get("x") y = entity.get("y") pixel_x = x * self.tile_size[0] + self.tile_size[0] // 2 pixel_y = y * self.tile_size[1] + self.tile_size[1] // 2 if entity_type not in self.images: image_file = f"{entity_type}.png" image = pygame.image.load(image_file) self.images[entity_type] = image image = self.images[entity_type] self.screen.blit(image, (pixel_x - image.get_width() // 2, pixel_y - image.get_height() // 2)) def client_loop(self): """主循环:处理事件、绘制画面""" try: # 启动实体更新线程,设置为守护线程 entity_thread = threading.Thread(target=self.entity_loop, daemon=True) entity_thread.start() clock = pygame.time.Clock() run = True while run: # 处理事件 for event in pygame.event.get(): if event.type == pygame.QUIT: run = False elif event.type == pygame.MOUSEBUTTONDOWN: print("鼠标按键按下") elif event.type == pygame.KEYDOWN: print("键盘按键按下") # 绘制画面 self.screen.fill((0, 0, 0)) tile_map = self.get_tile_map() self.draw_tile_map(tile_map) self.draw_entities() pygame.display.update() # 控制帧率为60FPS clock.tick(60) pygame.quit() sys.exit() except EmptyJSONError as e: print(f"错误: {e} 来源: {e.request_url}") def entity_loop(self): """实体更新子线程:定期从服务器获取实体数据""" try: while True: self.get_entities() time.sleep(0.1) except Exception as e: print(f"错误: {e}") screen_size = (320, 320) tile_size = (32, 32) green = (0, 255, 0) # 启动客户端 client = GameClient(server_url, screen_size, tile_size, green) client.client_loop()
关键修改说明
- 主线程统一处理绘制与事件:取消
draw_thread,将绘制逻辑移到主循环,符合Pygame线程安全要求。 - 线程锁保护共享资源:使用
with self.lock包裹对self.entities的读写操作,避免数据竞争。 - 稳定帧率控制:使用
pygame.time.Clock().tick(60)控制帧率,替代time.sleep,既保证画面流畅又避免CPU占用过高。 - 守护线程设置:实体线程设为守护线程,主线程退出时自动终止,防止程序残留。
- 事件逻辑修正:将
pygame.quit改为pygame.QUIT,导入sys模块,确保退出逻辑正常执行。
内容的提问来源于stack exchange,提问作者Ryan Lupton
相关产品推荐
相关产品推荐

