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

Pygame游戏窗口点击后无响应问题求助

Pygame客户端点击窗口无响应问题修复

问题背景

用Pygame开发连接服务器的游戏客户端,服务器返回玩家、怪物等实体列表,窗口负责绘制并更新状态。但点击窗口后会停止响应,尝试模拟实体、关闭相关线程都无法解决,怀疑主循环存在问题。

核心问题分析

  1. Pygame线程安全限制:Pygame的图形绘制API(如blit、display.update)和事件处理必须在主线程执行,原代码将draw_loop放在子线程运行,会导致图形上下文混乱,引发窗口无响应。
  2. 主循环无帧率控制:原主循环未添加延迟,会疯狂占用CPU资源,导致系统无法及时处理窗口事件(如点击、关闭)。
  3. 事件处理错误:event.type == pygame.quit拼写错误,应为pygame.QUIT;且未导入sys模块,调用sys.exit()会触发报错。
  4. 共享资源无锁保护:子线程entity_loop修改self.entities时,主线程可能同时读取该列表,引发数据竞争,导致绘制异常或卡死。

修复方案

  1. 将绘制逻辑整合到主线程主循环中,取消独立的draw_thread。
  2. 给主循环添加帧率控制,避免CPU占用过高。
  3. 修正事件处理错误,导入缺失的sys模块。
  4. 为共享的self.entities列表添加线程锁,确保读写安全。
  5. 将子线程设置为守护线程,主线程退出时自动终止子线程。

修复后完整代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 11:25:55