Pygame简易沙盒游戏生成大量方块后帧率过低问题排查
嘿,很高兴看到你在打造自己的沙盒模拟游戏!你怀疑分开的列表存储是卡顿原因,这点没错,但还有几个关键的性能瓶颈咱们一起解决:
1. 优化数据结构:用统一的列表存储方块信息
你现在用cubex、cubey、cubec三个分开的列表来存方块的位置和颜色,每次遍历都要同步操作三个列表,不仅代码繁琐,也会增加内存访问的开销。建议把每个方块的所有信息打包成一个字典,存在一个列表里:
# 初始化方块列表,每个元素是一个字典 blocks = [] # 生成方块时直接添加字典 x = round((mouse[0]/cubew),0)*cubew y = round((mouse[1]/cubew),0)*cubew blocks.append({ 'x': x, 'y': y, 'color': color, 'rect': pygame.Rect(x, y, cubew, cubew) # 提前创建Rect,后续碰撞检测复用 })
这样遍历的时候只需要操作一个列表,代码更清晰,性能也更好。
2. 降低碰撞检测的复杂度(核心瓶颈)
你现在的碰撞检测是**O(n²)**的:每个方块都要和其他所有方块检查碰撞,当方块数量到几百个时,计算量会爆炸式增长。这里有两个优化方向:
2.1 提前缓存方块Rect
每次碰撞检测都创建新的pygame.Rect是不必要的,我们可以把Rect直接存在方块字典里,更新位置时同步更新Rect的坐标:
# 更新方块位置时同步更新Rect block['y'] -= gravity block['rect'].y = block['y']
2.2 用空间分区减少检测范围
把游戏区域分成和方块大小一致的网格,每个网格只存储当前在里面的方块。这样检查某个方块的下方/周围时,只需要查询相邻的网格,不用遍历所有方块:
# 初始化网格(按方块大小10px划分) grid_width = (wnx - 154) // cubew # 只计算游戏区域的网格 grid_height = floory // cubew grid = [[[] for _ in range(grid_height)] for _ in range(grid_width)] # 每次更新方块位置后,更新网格 def update_grid(blocks, grid, cubew): # 先清空网格 for row in grid: row.clear() # 把每个方块放到对应的网格格子里 for idx, block in enumerate(blocks): # 转换为游戏区域内的网格坐标 grid_x = (block['x'] - 154) // cubew grid_y = block['y'] // cubew if 0 <= grid_x < len(grid) and 0 <= grid_y < len(grid[0]): grid[grid_y][grid_x].append(idx) # 检测下方碰撞时,只查当前方块下方的网格格子 current_grid_x = (block['x'] - 154) // cubew current_grid_y = block['y'] // cubew collided = False # 检查下方格子里的方块 if current_grid_y + 1 < len(grid): nearby_block_indices = grid[current_grid_y + 1][current_grid_x] for idx in nearby_block_indices: if idx != current_idx and block['rect'].colliderect(blocks[idx]['rect']): collided = True break
3. 优化绘制逻辑
你现在逐个调用cube()函数绘制方块,当数量多的时候会很慢。可以用pygame.sprite.Sprite和pygame.sprite.Group来批量绘制,这是pygame专门优化过的绘制方式:
class Block(pygame.sprite.Sprite): def __init__(self, x, y, color, size): super().__init__() self.image = pygame.Surface((size, size)) self.image.fill(color) self.rect = self.image.get_rect(topleft=(x, y)) self.color = color # 初始化精灵组 block_group = pygame.sprite.Group() # 添加方块时创建Sprite并加入组 new_block = Block(x, y, color, cubew) block_group.add(new_block) # 绘制时只需要调用一次,pygame会批量处理 block_group.draw(wn)
4. 修复代码里的小bug
你的事件处理有个潜在问题:在toggle_fast == False的分支里,你用了if event.type == pygame.MOUSEBUTTONDOWN,但这个event只在外面的for event in pygame.event.get()循环里有效,循环结束后event变量可能不存在,会导致报错。应该把这个逻辑移到事件循环内部:
while not exit: clickt = False mouse = pygame.mouse.get_pos() click = pygame.mouse.get_pressed() for event in pygame.event.get(): if event.type == pygame.QUIT: exit = True if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: toggle_fast = not toggle_fast if event.key == pygame.K_v: erase = not erase # 把鼠标点击生成方块的逻辑移到这里 if event.type == pygame.MOUSEBUTTONDOWN and not toggle_fast: if mouse[1] < floory and mouse[0] >= 154: x = round((mouse[0]/cubew),0)*cubew y = round((mouse[1]/cubew),0)*cubew new_block = Block(x, y, color, cubew) block_group.add(new_block)
总结
最影响性能的是O(n²)的碰撞检测,其次是零散的数据存储。按照上面的方法优化后,即使几千个方块也能保持流畅。如果你刚学Python,不用一下子全部改完,可以先从统一数据结构和修复事件bug开始,再逐步优化碰撞检测。
内容的提问来源于stack exchange,提问作者vLev

