Will converting to PySDL2 make my app run faster t

2019-09-15 05:38发布

问题:

I've written a little toy in Python using Pygame. It generates critters (a circle with a directional line, not an image) to wander around the screen. I'm interested in making it more sophisticated, but I'm running into serious performance problems. As the number of critters on the screen passes 20, the frame rate drops rapidly from 60fps as far as 11fps with 50 on the screen. I've gone over my (very simple) code a number of different ways, even profiling with cProfile, without finding any way to optimize.

To make a long story somewhat less long, I think I've concluded PyGame just isn't cut out for what I'm asking it to do. Consequently, I'm looking to convert to something else. C++ is the obvious answer, but as this is just a toy I'd rather code in Python, if possible. Especially because it's already written.

In looking at C++, I discovered that there's an SDL (wrapper? bindings? Not sure the term) for Python: PySDL2.

Thanks for sticking with me. Now the payoff: is there any reason to believe that converting my app to use PySDL2 will make it faster? Especially considering PyGame apparently uses SDL under the hood (somehow).

EDIT: As requested:

import pygame
from pygame import gfxdraw
import pygame.locals
import os
import math
import random
import time

(INSERT CONTENTS OF VECTOR.PY FROM https://gist.github.com/mcleonard/5351452 HERE)


pygame.init()

#some global constants
BLUE = (0, 0, 255)
WHITE = (255,255,255)
diagnostic = False
SPAWN_TIME = 1 #number of seconds between creating new critters
FLOCK_LIMIT = 30 #number of critters at which the flock begins being culled
GUIDs = [0] #list of guaranteed unique IDs for identifying each critter

# Set the position of the OS window
position = (30, 30)
os.environ['SDL_VIDEO_WINDOW_POS'] = str(position[0]) + "," + str(position[1])

# Set the position, width and height of the screen [width, height]
size_x = 1000
size_y = 500
size = (size_x, size_y)
FRAMERATE = 60
SECS_FOR_DYING = 1
screen = pygame.display.set_mode(size)
screen.set_alpha(None)
pygame.display.set_caption("My Game")

# Used to manage how fast the screen updates
clock = pygame.time.Clock()


def random_float(lower, upper):
    num = random.randint(lower*1000, upper*1000)
    return num/1000


def new_GUID():
    num = GUIDs[-1]
    num = num + 1
    while num in GUIDs:
        num += 1
    GUIDs.append(num)
    return num


class HeatBlock:
    def __init__(self,_tlx,_tly,h,w):
        self.tlx = int(_tlx)
        self.tly = int(_tly)
        self.height = int(h)+1
        self.width = int(w)
        self.heat = 255.0
        self.registered = False

    def register_tresspasser(self):
        self.registered = True
        self.heat = max(self.heat - 1, 0)

    def cool_down(self):
        if not self.registered:
            self.heat = min(self.heat + 0.1, 255)
        self.registered = False

    def hb_draw_self(self):
        screen.fill((255,int(self.heat),int(self.heat)), [self.tlx, self.tly, self.width, self.height])


class HeatMap:
    def __init__(self, _h, _v):
        self.h_freq = _h #horizontal frequency
        self.h_rez = size_x/self.h_freq #horizontal resolution
        self.v_freq = _v #vertical frequency
        self.v_rez = size_y/self.v_freq #vertical resolution
        self.blocks = [] 

    def make_map(self):
        h_size = size_x/self.h_freq
        v_size = size_y/self.v_freq
        for h_count in range(0, self.h_freq):
            TLx = h_count * h_size #TopLeft corner, x
            col = []
            for v_count in range(0, self.v_freq):
                TLy = v_count * v_size #TopLeft corner, y
                col.append(HeatBlock(TLx,TLy,v_size,h_size))
            self.blocks.append(col)

    def hm_draw_self(self):
        for col in self.blocks:
            for block in col:
                block.cool_down()
                block.hb_draw_self()

    def register(self, x, y):
        #convert the given coordinates of the trespasser into a col/row block index
        col = max(int(math.floor(x / self.h_rez)),0)
        row = max(int(math.floor(y / self.v_rez)),0)
        self.blocks[col][row].register_tresspasser()


class Critter:
    def __init__(self):
        self.color = (random.randint(1, 200), random.randint(1, 200), random.randint(1, 200))
        self.linear_speed = random_float(20, 100)
        self.radius = int(round(10 * (100/self.linear_speed)))
        self.angular_speed = random_float(0.1, 2)
        self.x = int(random.randint(self.radius*2, size_x - (self.radius*2)))
        self.y = int(random.randint(self.radius*2, size_y - (self.radius*2)))
        self.orientation = Vector(0, 1).rotate(random.randint(-180, 180))
        self.sensor = Vector(0, 20)
        self.sensor_length = 20
        self.new_orientation = self.orientation
        self.draw_bounds = False
        self.GUID = new_GUID()
        self.condition = 0 #0 = alive, [1-fps] = dying, >fps = dead
        self.delete_me = False

    def c_draw_self(self):
        #if we're alive and not dying, draw our normal self
        if self.condition == 0:
            #diagnostic
            if self.draw_bounds:
                pygame.gfxdraw.rectangle(screen, [int(self.x), int(self.y), 1, 1], BLUE)
                temp = self.orientation * (self.linear_speed * 20)
                pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x + temp[0]), int(self.y + temp[1]), BLUE)
            #if there's a new orientation, match it gradually
            temp = self.new_orientation * self.linear_speed
            #draw my body
            pygame.gfxdraw.aacircle(screen, int(self.x), int(self.y), self.radius, self.color)
            #draw a line indicating my new direction
            pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x + temp[0]), int(self.y + temp[1]), BLUE)
            #draw my sensor (a line pointing forward)
            self.sensor = self.orientation.normalize() * self.sensor_length
            pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x + self.sensor[0]), int(self.y + self.sensor[1]), BLUE)
        #otherwise we're dying, draw our dying animation
        elif 1 <= self.condition <= FRAMERATE*SECS_FOR_DYING:
            #draw some lines in a spinningi circle
            for num in range(0,10):
                line = Vector(0, 1).rotate((num*(360/10))+(self.condition*23))
                line = line*self.radius
                pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x+line[0]), int(self.y+line[1]), self.color)

    def print_self(self):
        #diagnostic
        print("==============")
        print("radius:", self.radius)
        print("color:", self.color)
        print("linear_speed:", self.linear_speed)
        print("angular_speed:", self.angular_speed)
        print("x:", self.x)
        print("y:", int(self.y))
        print("orientation:", self.orientation)

    def avoid_others(self, _flock):
        for _critter in _flock:
            #if the critter isn't ME...
            if _critter.GUID is not self.GUID and _critter.condition == 0:
                #and it's touching me...
                if self.x - _critter.x <= self.radius + _critter.radius:
                    me = Vector(self.x, int(self.y))
                    other_guy = Vector(_critter.x, _critter.y)
                    distance = me - other_guy

                    #give me new orientation that's away from the other guy
                    if distance.norm() <= ((self.radius) + (_critter.radius)):
                        new_direction = me - other_guy
                        self.orientation = self.new_orientation = new_direction.normalize()

    def update_location(self, elapsed):
        boundary = '?'
        while boundary != 'X':
            boundary = self.out_of_bounds()
            if boundary == 'N':
                self.orientation = self.new_orientation = Vector(0, 1).rotate(random.randint(-20, 20))
                self.y = (self.radius) + 2
            elif boundary == 'S':
                self.orientation = self.new_orientation = Vector(0,-1).rotate(random.randint(-20, 20))
                self.y = (size_y - (self.radius)) - 2
            elif boundary == 'E':
                self.orientation = self.new_orientation = Vector(-1,0).rotate(random.randint(-20, 20))
                self.x = (size_x - (self.radius)) - 2
            elif boundary == 'W':
                self.orientation = self.new_orientation = Vector(1,0).rotate(random.randint(-20, 20))
                self.x = (self.radius) + 2
            point = Vector(self.x, self.y)
            self.x, self.y = (point + (self.orientation * (self.linear_speed*(elapsed/1000))))
            boundary = self.out_of_bounds()

    def update_orientation(self, elapsed):
        #randomly choose a new direction, from time to time
        if random.randint(0, 100) > 98:
            self.choose_new_orientation()
        difference = self.orientation.argument() - self.new_orientation.argument()
        self.orientation = self.orientation.rotate((difference * (self.angular_speed*(elapsed/1000))))

    def still_alive(self, elapsed):
        return_value = True #I am still alive
        if self.condition == 0:
            return_value = True
        elif self.condition <= FRAMERATE*SECS_FOR_DYING:
            self.condition = self.condition + (elapsed/17)
            return_value = True
        if self.condition > FRAMERATE*SECS_FOR_DYING:
            return_value = False

        return return_value

    def choose_new_orientation(self):
        if self.new_orientation:
            if (self.orientation.argument() - self.new_orientation.argument()) < 5:
                rotation = random.randint(-300, 300)
                self.new_orientation = self.orientation.rotate(rotation)

    def out_of_bounds(self):
        if self.x >= (size_x - (self.radius)):
            return 'E'
        elif self.y >= (size_y - (self.radius)):
            return 'S'
        elif self.x <= (0 + (self.radius)):
            return 'W'
        elif self.y <= (0 + (self.radius)):
            return 'N'
        else:
            return 'X'


# -------- Main Program Loop -----------
# generate critters
flock = [Critter()]
heatMap = HeatMap(60, 40)
heatMap.make_map()
last_spawn = time.clock()
run_time = time.perf_counter()
frame_count = 0
max_time = 0
ms_elapsed = 1
avg_fps = [1]
# Loop until the user clicks the close button.
done = False
while not done:
    # --- Main event loop only processes one event
    frame_count = frame_count + 1
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    # --- Game logic should go here
    #check if it's time to make another critter
    if time.clock() - last_spawn > SPAWN_TIME:
        flock.append(Critter())
        last_spawn = time.clock()
    if len(flock) >= FLOCK_LIMIT:
        #if we're over the flock limit, cull the herd
        counter = FLOCK_LIMIT
        for critter in flock[0:len(flock)-FLOCK_LIMIT]:
            #this code allows a critter to be "dying" for a while, to play an animation
            if critter.condition == 0:
                critter.condition = 1
            elif not critter.still_alive(ms_elapsed):
                critter.delete_me = True
    counter = 0

    #delete all the critters that have finished dying
    while counter < len(flock):
        if flock[counter].delete_me:
            del flock[counter]
        else:
            counter = counter+1

    #----loop on all critters once, doing all functions for each critter
    for critter in flock:
        if critter.condition == 0:
            critter.avoid_others(flock)
            if critter.condition == 0:
                heatMap.register(critter.x, critter.y)
            critter.update_location(ms_elapsed)
            critter.update_orientation(ms_elapsed)
            if diagnostic:
                critter.print_self()

    #----alternately, loop for each function. Speed seems to be similar either way
    #for critter in flock:
    #    if critter.condition == 0:
    #        critter.update_location(ms_elapsed)
    #for critter in flock:
    #    if critter.condition == 0:
    #        critter.update_orientation(ms_elapsed)

    # --- Screen-clearing code goes here

    # Here, we clear the screen to white. Don't put other drawing commands
    screen.fill(WHITE)

    # --- Drawing code should go here
    #draw the heat_map
    heatMap.hm_draw_self()
    for critter in flock:
        critter.c_draw_self()

    #draw the framerate
    myfont = pygame.font.SysFont("monospace", 15)
    #average the framerate over 60 frames
    temp = sum(avg_fps)/float(len(avg_fps)) 
    text = str(round(((1/temp)*1000),0))+"FPS | "+str(len(flock))+" Critters"
    label = myfont.render(text, 1, (0, 0, 0))
    screen.blit(label, (5, 5))

    # --- Go ahead and update the screen with what we've drawn.
    pygame.display.update()

    # --- Limit to 60 frames per second
    #only run for 30 seconds
    if time.perf_counter()-run_time >= 30:
        done = True
    #limit to 60fps
    #add this frame's time to the list
    avg_fps.append(ms_elapsed)
    #remove any old frames
    while len(avg_fps) > 60:
        del avg_fps[0]
    ms_elapsed = clock.tick(FRAMERATE)
    #track longest frame
    if ms_elapsed > max_time:
        max_time = ms_elapsed

#print some stats once the program is finished
print("Count:", frame_count)
print("Max time since last flip:", str(max_time)+"ms")
print("Total Time:", str(int(time.perf_counter()-run_time))+"s")
print("Average time for a flip:", str(int(((time.perf_counter()-run_time)/frame_count)*1000))+"ms")
# Close the window and quit.
pygame.quit()
标签: pygame pysdl2