这个PyGame代码能否以大于40个生物的速度运行60fps? [英] Can this PyGame code run 60fps for >40 critters?

查看:82
本文介绍了这个PyGame代码能否以大于40个生物的速度运行60fps?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

https://gist.github.com/mcleonard/5351452 ),而cProfile在此没有太多要说的功能.如果您想使其可运行,我已在代码中提供了一个链接.

In my other question, some of the posters asked to see the code and suggested I make a new question. As requested, here is most of the code I'm using. I've removed the Vector class, simply because it's a lot of code. It's well-understood math that I got from someone else (https://gist.github.com/mcleonard/5351452), and cProfile didn't have much to say about any of the functions there. I've provided a link in the code, if you want to make this run-able.

如果将向量类粘贴到代码中指示的位置,则此代码应运行.

This code should run, if you paste the the vector class where indicated in the code.

问题是,一旦超过20个生物,帧速率就会从60fps迅速下降到约50个生物的11fps.

The problem is, once I get above 20 critters, the framerate drops rapidly from 60fps to 11fps around 50 critters.

请原谅意大利面条代码.其中大部分是诊断性混接或预编码,我打算删除它们或将其转变为某种行为(而不是硬编码的值).

Please excuse the spaghetti-code. Much of this is diagnostic kludging or pre-code that I intend to either remove, or turn into a behavior (instead of a hard-coded value).

此应用基本上由4个对象组成.

This app is basically composed of 4 objects.

Vector对象提供抽象的向量操作.

A Vector object provides abstracted vector operations.

加热块可以跟踪其自身的热量"水平,可以增加或减少热量.它也可以自己绘制.

A Heat Block is able to track it's own "heat" level, increase it and decrease it. It can also draw itself.

热图由平铺在屏幕上的热块组成.给定坐标后,可以选择这些坐标所在的块.

A Heat Map is composed of heat blocks which are tiled across the screen. When given coordinates, it can choose the block that those coordinates fall within.

小动物具有许多功能,使其能够在屏幕上四处游荡,撞墙和其他小动物,选择新的随机方向并死亡.

A Critter has many features that make it able to wander around the screen, bump off of the walls and other critters, choose a new random direction, and die.

主循环遍历群"中的每个小生物,并更新其条件"(是否正在死亡"),其位置,方向以及当前所处的加热块.循环还会在每个加热块上进行迭代,以使其冷却".

The main loop iterates through each critter in the "flock" and updates its "condition" (whether or not it's "dying"), its location, its orientation, and the heat block on which it is currently standing. The loop also iterates over each heat block to let it "cool down."

然后,主循环要求热图绘制自己,然后让羊群中的每个小动物绘制自己.

Then the main loop asks the heat map to draw itself, and then each critter in the flock to draw itself.

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

(I got a nice vector class from someone else. It's large, and mostly likely not the problem.)
(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()]
# generate heat map
heatMap = HeatMap(60, 40)
heatMap.make_map()
# set some settings
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()

推荐答案

您已经可以提高性能的一件事是使用

One thing you can already do to improve the performance is to use pygame.math.Vector2 instead of your Vector class, because it's implemented in C and therefore faster. Before I switched to pygame's vector class, I could have ~50 critters on the screen before the frame rate dropped below 60, and after the change up to ~100.

pygame.math.Vector2没有该argument方法,因此您需要从类中提取它并将其转换为函数:

pygame.math.Vector2 doesn't have that argument method, so you need to extract it from the class and turn it into a function:

def argument(vec):
    """ Returns the argument of the vector, the angle clockwise from +y."""
    arg_in_rad = math.acos(Vector(0,1)*vec/vec.length())
    arg_in_deg = math.degrees(arg_in_rad)
    if vec.x < 0:
        return 360 - arg_in_deg
    else:
        return arg_in_deg

然后在程序中的任何地方将.norm()更改为.length().

And change .norm() to .length() everywhere in the program.

此外,在while循环之前定义字体对象(myfont).这只是一个小改进,但是每一帧都很重要.

Also, define the font object (myfont) before the while loop. That's only a minor improvement, but every frame counts.

这篇关于这个PyGame代码能否以大于40个生物的速度运行60fps?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆