为什么我的 pygame 代码在一段时间后变得非常缓慢?有没有办法让它运行得更快?

问题描述

我编写了一个程序来模拟一种称为扩散受限聚集的现象,使用 pygame 中正方形的随机运动。这个想法是有一个茎,每个接触它的粒子(正方形)都会粘在上面并成为茎的一部分。

代码似乎可以工作,但是大约 30 秒到 1 分钟后,它开始变慢了很多。我不知道为什么。

import pygame
import random


#changing the row number will change the size of the squares,and bassically the size of the invisible 'array'
width = 1000
rows = 500
d = width//rows
e = {}
squares = []
accreted = []


#note: all positions are noted in array-like notation (a matrix of dimensions rows x rows)

#to convert it back to normal notation,do (square.position[0] * d,square.position[1] * d)


class square:

    def __init__(self,position):

        self.position = (position)

#method to move a square in a random direction (no diagonal) (brownian motion)

    def move(self):
        a = random.randint(0,3)
        if a == 0:
            new_posi = (self.position[0] + 1)
            new_posj = (self.position[1])
        elif a == 1:
            new_posi = (self.position[0] - 1)
            new_posj = (self.position[1])
        elif a == 2:
            new_posi = (self.position[0])
            new_posj = (self.position[1] + 1)
        else:
            new_posi = (self.position[0])
            new_posj = (self.position[1] - 1)

        if new_posj<0 or new_posi<0 or new_posi>rows or new_posj>rows:
            self.move()
        else:
            self.position = (new_posi,new_posj)
            pygame.draw.rect(win,(255,255,255),[new_posi * d,new_posj * d,d,d])


def accrete(square):
    accreted.append(square)
    if square in squares:
        squares.remove(square)

def redrawWindow(win):
    win.fill((0,0))
    pygame.draw.rect(win,[stem.position[0] * d,stem.position[1] * d,d])
    for square in squares:
        square.move()

        # here we make it so that every square that touches the stem stops moving,then a square that touches this square stops moving,etc.

        for accret in accreted:
            if square.position[1] == accret.position[1]+1 and square.position[0] == accret.position[0]:
                accrete(square)
            elif square.position[1] == accret.position[1]-1 and square.position[0] == accret.position[0]:
                accrete(square)
            elif square.position[1] == accret.position[1] and square.position[0] == accret.position[0]+1:
                accrete(square)
            elif square.position[1] == accret.position[1] and square.position[0] == accret.position[0]-1:
                accrete(square)
    for accret in accreted:
        pygame.draw.rect(win,[accret.position[0] * d,accret.position[1] * d,d])
    pygame.display.update()

def main():
    global win
    win = pygame.display.set_mode((width,width))
    clock = pygame.time.Clock()

    while True:
        # pygame.time.delay(5)
        # clock.tick(64)
        redrawWindow(win)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()


#by changing the range here,we change how many squares are created

for i in range(5000):
    e["accreted{0}".format(i)] = square((random.randint(0,rows),random.randint(0,rows)))
    squares.append(e["accreted{0}".format(i)])

 #a stem to start accretion from


stem = square((rows/2,rows/2))
accrete(stem)

main()

解决方法

我已经在我的 MacBook Pro 上观看了您的模拟运行了一段时间。似乎在我的系统上,它花费的时间比一分钟要长得多......也许更像是 5 左右......在它开始明显变慢之前。但它确实如此。

我认为问题在于您在模拟过程中构建了这种“吸积”结构,因此,“吸积”方块(存储在 accreted 列表中的那些)的数量不断增加。对于程序的每次迭代,您的代码需要将每个活动方块的位置与每个“吸积”方块的位置进行比较。因此,随着时间的推移,您必须进行的比较次数会不断增加。

如果您希望能够在模拟进程中保持更新速度(帧速率),您可能需要找到一种优化算法的方法。您需要弄清楚如何更聪明地进行比较,以某种方式避免随着您正在构建的结构的增长而出现的迭代时间几何级数增长。

更新和可能的优化:我看到了一个非常简单的优化,您可以将其添加到代码中以大大加快速度。你可以做的是在你的“吸积”方块周围保持一个边界框。向该列表中添加新方块时,如有必要,可增加边界框的大小,使其包含新方块。现在,当您第一次检查活动方块和吸积方块列表之间是否发生碰撞时,您可以先检查活动方块是否在吸积方块的边界框内(视情况留有一点额外的边距),然后再进行测试对于那个方块和任何一个吸积方块之间的碰撞。这将让您立即排除大多数活动方块和吸积方块之间的碰撞,只需对每个活动方块进行一次碰撞测试。这应该具有让您的代码在后面几轮中保持与早期几轮一样快的效果,因为无论增加的结构有多大,大多数活动方块总是会被简单地拒绝为碰撞候选者。

更新 2:我所描述的绝对是您的代码发生了什么。我添加了一些代码来计算您在模拟游戏的每一轮中执行的碰撞​​测试次数。以下是以一秒为间隔执行的测试数量,以及您的 sim 的一次迭代所花费的时间(以秒为单位):

0 5000 0.023629821000000106
1 9998 0.023406135000000106
2 24980 0.03102543400000002
...
30 99680 0.07482247300000111
31 99680 0.08382184299999551
...
59 114563 0.08984024400000123
60 114563 0.087317634999998

您的代码的第一次迭代按预期进行了 5000 次命中测试,而您的 sim 单次迭代大约需要 0.023 秒。一分钟后,您的 sim 每次迭代必须进行 20 倍以上的测试,114563,而现在单次迭代需要 0.087 秒。这个问题一直在增长,而您的代码却在不断变慢。 (有趣的是,您的模拟最多一分钟的“进步”大部分发生在前 30 秒内。对于这次运行,在第二个 30 秒间隔内仅发生了 3 次增加。)

,

第一个也是可能最好的优化是保留一个“禁止”方块列表,这些方块将触发粒子的冻结,而不是多次迭代 accreted 中的所有位置。

所以例如当我们从第一个词干粒子(或正方形,或随便你怎么称呼它)开始时,我们还将该粒子位置的上方、下方、左侧和右侧的位置存储在 set 中。使用 set 很重要,因为在集合中查找项目比使用 list 快得多。

移动一个方块后,我们现在检查那个方块的新位置是否在这个集合中。如果是,我们也将所有相邻位置添加到该集合中。

接下来我们可以改进的是方块列表本身。我们不是从列表中删除一个冻结的方格,而是每帧创建一个新列表,并添加本轮未冻结的所有方格。 (您的代码实际上是有问题的,因为没有制作正方形列表的副本并在从原始列表中删除项目时迭代该副本)我将使用 deque 因为添加到它的速度比常规列表稍快.

另一个瓶颈是您在每一帧中创建的大量随机数。 random.randint() 变得非常缓慢。我们可以在脚本开始时创建一个随机数列表并使用它,这样我们就不必在运行时创建新的随机数。

我们还可以更改绘图。使用 pygame.draw.rect 5000 次也很慢。让我们创建一个表面并使用 pygame 的新批处理函数 Surface.blits 对其进行 blit(我想使用 pygame.surfarray 直接操作屏幕表面会更快)。

在下面的代码中,我还实现了 CryptoFool 建议的边界框,因为为什么不这样做,但最大的加速是使用我上面描述的集合。

通过这些更改,我获得了大约 200 FPS,而且随着时间的推移没有任何减速:

import pygame
import numpy as np
import random
from collections import deque

def get_rand_factory():
    length = 100000000
    sample = np.random.randint(1,5,length).tolist()
    index = -1
    def inner():
        nonlocal index
        index += 1
        if index == length:
            index = 0
        return sample[index]
    return inner

get_rand = get_rand_factory()

def move(x,y,rows):
    dx,dy = x,y
    a = get_rand()
    if a == 1: dx += 1
    elif a == 2: dx -= 1
    elif a == 3: dy += 1
    else: dy -= 1

    if dx<0 or dy<0 or dx>rows or dy>rows:
        return move(x,rows)
    return dx,dy

def get_adjacent(x,y):
    for dx,dy in (1,0),(-1,(0,1),-1):
        yield x + dx,y + dy 

def get_bounds(positions):
    min_x,min_y,max_x,max_y = 9999,9999,0
    for x,y in positions:
        min_x = min(min_x,x)
        min_y = min(min_y,y)
        max_x = max(max_x,x)
        max_y = max(max_y,y)
    return min_x,max_y

def main():

    width = 1000
    rows = 500
    d = width//rows
    squares = deque()
    accreted = set()
    adjacent_accreted = set()

    win = pygame.display.set_mode((width,width))
    clock = pygame.time.Clock()

    for i in range(5000):
        pos = (random.randint(0,rows),random.randint(0,rows))
        squares.append(pos)

    stem = (rows/2,rows/2)
    accreted.add(stem)
    adjacent_accreted.add(stem)
    for adj in get_adjacent(*stem):
        adjacent_accreted.add(adj)

    rect_white = pygame.Surface((d,d))
    rect_white.fill('white')

    rect_blue = pygame.Surface((d,d))
    rect_blue.fill((255,255))

    bounds = get_bounds(adjacent_accreted)
    min_x,max_y = bounds

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        win.fill((0,0))

        new_state = deque()
        for x,y in squares:
            die = False
            new_pos = move(x,rows)

            if min_x <= new_pos[0] <= max_x and min_y <= new_pos[1] <= max_y:
                if new_pos in adjacent_accreted:
                    accreted.add(new_pos)
                    adjacent_accreted.add(new_pos)
                    for adj in get_adjacent(*new_pos):
                        adjacent_accreted.add(adj)
                    die = True
                    bounds = get_bounds(adjacent_accreted)
                    min_x,max_y = bounds     
            if not die:
                new_state.append(new_pos)
                
        squares = new_state

        win.blits(blit_sequence=((rect_blue,(pos[0]*d,pos[1]*d)) for pos in accreted))
        win.blits(blit_sequence=((rect_white,pos[1]*d)) for pos in squares))

        pygame.draw.rect(win,255,255),[bounds[0] * d,bounds[1] * d,(bounds[2]-bounds[0]) * d,(bounds[3]-bounds[1]) * d],1)
            
        pygame.display.update()
        pygame.display.set_caption(f'{clock.get_fps():.2f} {len(squares)=} {len(accreted)=}')
        clock.tick()
 
main()

enter image description here

,

看看你的代码:

def accrete(square):
    accreted.append(square)
    if square in squares:
        squares.remove(square)

squares 是一个包含最多 5000 个项目的列表。搜索任何内容最多需要 5000 次比较,因为没有索引,必须检查所有项目,直到在列表中找到它。如果条目是唯一的并且顺序无关紧要,请改用集合。索引和搜索项目中的集合运行速度非常快。