[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

Re: [pygame] sprite engine Top this!



Christopher Arndt schrieb:
Christopher Arndt wrote:
DR0ID wrote:

If you can make this faster, post your results here!
Yes, I can!
Pretty impressive!


I'm wondering which fps you get on your machines. Please tell me so I
can build a little statistic.
~425 Fps max here on Pentium III Mobile 1200MHz with 512MB

Fullscreen mode is maybe 3% slower.

I forgot:

Ubuntu Breezy, Python 2.4.2, pygame 1.7.1, SDL 1.2.7, Xorg 6.8.2

Hi

thank you for your results. As you suggested I have added the max, min and average fps printed out at exiting the code. I found it strange that there are some 0 fps counts!! (maybe at startup?)

And also sometimes it seems to drop nearly 0.0 fps in the middle of running and I do not know why! Can it be because of the garbage collector? Or other memory related stuff?

~DR0ID
#   
# (C) DR0ID 03.2006
# 
# you can contact me at: http://www.mypage.bluewin.ch/DR0ID/pygame
#
# or on IRC: irc.freenode.net 6667 #pygame 
#
#







import pygame
from pygame.locals import *
import random
import sprite as sprite
import time






__author__      = 'DR0ID'
__version__     = '0.2'	# change also top of this file!!
__versionnumber__  = 0,2   # versionnumber seperatly for comparing 
__license__     = 'public domain'
__copyright__   = '(c) DR0ID 05.2006'



class Quadrat(sprite.NewSprite):
    
    def __init__(self, register = True):
        sprite.NewSprite.__init__(self,(), register)
        self.image = pygame.Surface((30,30), pygame.SRCALPHA, 32).convert_alpha()
        self.image.fill( (random.randint(0,255),random.randint(0,255),random.randint(0,255),128) )
##        self.image.set_alpha(128)
##        self.image = self.image.convert_alpha()
        self.rect = self.image.get_rect()
        self.dirty = 2
        self.vx = 0
        while 0==self.vx:
            self.vx= random.randint(-3,3)
        self.vy = 0
        while 0==self.vy:
            self.vy = random.randint(-3,3)
        self.path_factor = 0.01
        self.maxX = (1-self.path_factor)*pygame.display.get_surface().get_width()
        self.minX = self.path_factor*pygame.display.get_surface().get_width()
        self.maxY = (1-self.path_factor)*pygame.display.get_surface().get_height()
        self.minY = self.path_factor*pygame.display.get_surface().get_height()
        self.rect.center = (random.randint(300,600),random.randint(300,400))
        
    def update(self):
        if self.rect.centerx > self.maxX or self.rect.centerx< self.minX:
            self.vx = -self.vx
        if self.rect.centery > self.maxY or self.rect.centery< self.minY:
            self.vy = -self.vy
        self.rect.move_ip(self.vx, self.vy)
        

class StaticQuadrat(sprite.NewSprite):
    
    def __init__(self, pos, size):
        sprite.NewSprite.__init__(self,(), False)
        self.image = pygame.Surface(size, pygame.SRCALPHA, 32).convert_alpha()
        self.image.fill((random.randint(0,255),random.randint(0,255),random.randint(0,255),128) )
        self.rect = pygame.Rect(pos, size)
        self.vx = self.vy =0
        self.dirty = 1
        self._renderer.add(self)
        
class Circle(Quadrat):
    
    def __init__(self):
        Quadrat.__init__(self, False)
        self.image = pygame.Surface((100,100), pygame.SRCALPHA, 32).convert_alpha()
        self.image.fill((0,0,0,0))
        pygame.draw.circle(self.image,(random.randint(0,255),random.randint(0,255),random.randint(0,255),128),(50,50) , 50)     
##        self.image.set_alpha(128)
##        self.image.convert_alpha()
        self._renderer.add(self)
        self.rect = self.image.get_rect()
        self.rect.center = (random.randint(300,600),random.randint(300,400))

        
class Text(sprite.NewSprite):
    
    def __init__(self, pos):
        sprite.NewSprite.__init__(self)
        self.dirty = 1
        self.font =  pygame.font.Font(pygame.font.get_default_font(), 24)
        self.image = self.font.render(str(0.0), 0, (255,255,255))
        self.image = self.font.render(str("Hit ESC to stop, ALT-ENTER to toggle fullscreen mode."), 0, (255,255,255))
        self.rect = self.image.get_rect()
        self.rect.move_ip(pos)
        self.ctr = 0
        self.nexttime = 0
        
        
    def update(self, now, s=''):
        self.ctr += 1
        if now > self.nexttime:
            if s=='':
                self.image = self.font.render(("%d FPS" % self.ctr), 0, (255,255,255))
                self.dirty = 1
                self.rect = self.image.get_rect(topleft=self.rect.topleft)
            self.ctr = 0
            self.nexttime = now + 1


#example 
#keys:
# s : stepmodus, press any key except s to step one frame furder
# d: change to debug modus
# f: change to slow 2 fps
# alt+enter: toggle fullscreen

def main():
    # setup pygame
    pygame.init()
    screen = pygame.display.set_mode((800,600),1,32)
    
    #prepare stuff, load images
    # convert them it will nearly double your fps (frames per second)
    # group for easier handling and add ouer objects
    g = pygame.sprite.OrderedUpdates()
    g.add(StaticQuadrat((100,300), (100,300)))
    for i in range(10):
        g.add(Quadrat())
    g.add(StaticQuadrat((50,450), (600,100)))
    g.add(Circle())
    g.add(StaticQuadrat((140,400), (300,300)))
##    for i in range(500):
##        g.add(StaticQuadrat((random.randint(0,800),random.randint(0,600)), (32,32)))

    text = Text((0,0))
    text2 = Text((150,0))
    sprite.Renderer.setBackgroundColor((0,0,80))
    pygame.display.set_caption("press 'h' for help (in console)")
    fpslist = []
    stepmodus = False
    fullscreen = False
    clock = pygame.time.Clock()
    while 1:
        # eventhandling keys
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                return
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
##                    print fpslist
                    max = 0
                    min = 1000
                    zero= 0
                    for r in fpslist:
                        if r>max:
                            max = r
                        if r<min:
                            if r == 0:
                                zero +=1
                            else:
                                min = r
                    print "max fps: ", max
                    print "min fpx: ", min
                    print "0 fps count:", zero
                    print "average fps: ", sum(fpslist)/len(fpslist)
                    pygame.quit() # if you run it from console....
                    return
                elif event.key == K_d:
                    sprite.Renderer.switchDEBUG()
                elif event.key == K_f:
                    if sprite.Renderer.fpsGet()==20:
                        sprite.Renderer.fpsSet(0)
                    else:
                        sprite.Renderer.fpsSet(20)
                elif event.key == K_s:
                    stepmodus = not stepmodus
                elif event.key == K_h:
                    print "Help:"
                    print "s  toggles stepmode, press any key except s to step"
                    print "d  toggles debugmode"
                    print "f  toggles to 20 fps"
                    print "alt+enter toggles fullscreen"
                    print "esc  to exit"
                    print "h  for this help"
                    
                elif event.key == pygame.K_RETURN:
                    if pygame.key.get_mods() & pygame.KMOD_ALT:
                        print "toggle fullscreen"
                        fullscreen = not fullscreen
                        if fullscreen:
                            flags = pygame.FULLSCREEN
                        else:
                            flags = 0
                        minbitdepth = 16
                        bitdepth = 32
                        size = screen.get_size()
                        bitdepth = pygame.display.mode_ok(size, flags, bitdepth)
                        if bitdepth < minbitdepth:
                            raise Exception("Your system is unable to display %d bit color in an %d x %d window!" % (minbitdepth, size[0], size[1]))
                        screen = pygame.display.set_mode(size, flags, bitdepth)
                        sprite.Renderer.notifyScreenChange()
                            
                            
                            
        if stepmodus:
            pygame.event.clear()
            event = pygame.event.wait()
            while event.type is not KEYUP:
                if event.type == (pygame.constants.KEYDOWN or pygame.constants.KEYUP):
                    if event.key == pygame.constants.K_s:
                        stepmodus = not stepmodus
                event = pygame.event.wait()
                
                    
        fpslist.append(clock.get_fps())  
        clock.tick()          
        text.update( time.time()) 
        g.update()
        sprite.Renderer.render()
    
    
    
    
#if __name__ == '__main__': main()
if __name__=="__main__":
	main()
import pygame



#==============================================================================
class NewSprite(pygame.sprite.Sprite):

#------------------------------------------------------------------------------
    
    def __init__(self, groups=(), register=True):
        pygame.sprite.Sprite.__init__(self, groups)
        self.dirty = 1
        self.rect = pygame.Rect(-99, -99, 0, 0)
        self._renderer = Renderer
        if register:
            self._renderer.add(self)
            
#------------------------------------------------------------------------------
##    def __setattr__(self, name, value):
##        pygame.sprite.Sprite.__setattr__(self, name, value)
##        if(name=="rect"):
##            pygame.sprite.Sprite.__setattr__(self, "area", self.rect.width*self.rect.height)

#------------------------------------------------------------------------------
    def kill(self):
        pygame.sprite.Sprite.kill(self)
        self._renderer.remove(self)
            
#------------------------------------------------------------------------------
    def getLayer(self):
        return self._renderer.getLayerOfSprite(self)
        
#------------------------------------------------------------------------------
    def changeLayer(self, toLayer, removeEmptyLayers = True):
        self._renderer.changeSpriteLayer(self, toLayer, removeEmptyLayers)
        
        
#------------------------------------------------------------------------------
#==============================================================================
#TODO: RLEACCEL flag and use convert() and conver_alpha() in resourcemanager!!        

class _Renderer(object):
        
        
    _instance = 0
#------------------------------------------------------------------------------
        
    def __init__(self):
        # Singleton pattern
        if self._instance is not 0:
            raise ReferenceError , "dont try to instanciate the Renderer, use .Renderer or _Renderer._instance"
        _Renderer._instance = self
        # Sprite lists
        self._sprites = {} # {spr:layerNr, ...} global list of sprites
        self._layers = {} # {layer:[spr,...]} global list of layers
        self._sortedLayers = [] # sorted layers self._layers.keys().sort()
        self._needSortLayers = True
        # Rect lists
##        self._lostRect = [] # removed sprites to clear
        self._spriteOldRect = {} # {sprite:rect} old position rects
        self._update = []
        # screen, background
        self._screen = 0
##        self._scree_rect = 0
        self._background = 0
        self._background_color = (255,0,255)
        self._getScreen = pygame.display.get_surface # function pointer
        self._display_update = pygame.display.update
        # timing
        self._clock = pygame.time.Clock()
        self._clock_tick = self._clock.tick
        self._fps = 0
        # init
        self._initialized = False
        # function pointers: .render(self) and .flip(self)
        self.render = self._init    # first time it points to _init() after that to _render()
        self.flip = self._flipInit  # first points to _flipInit() after that to _flip()
        
#------------------------------------------------------------------------------
            
    def switchDEBUG(self, fps=-1):
        if(self._initialized):
            if self.render == self._render:
                if -1!=fps:
                    self._fps = fps
                self.render = self._DEBUGrender
            else:
                self._flip()
                self.render = self._render
        else:
            raise TypeError, "Renderer not initialized!!!"
        
#dirty == 1 -> 0 clear and draw and reset dirty = 0 (this ones are on screen and need repaint, but perhaps next time not)
#dirty == 2 -> 2 clear and draw and do not reset dirty flag (for animation, allway needs repaint)
        
#------------------------------------------------------------------------------
        
    def add(self, *sprites):
        """
        Add a sprite to renderer. One can also pass a iterable sequence of sprites.
        """
        for sprite in sprites:
            if isinstance(sprite, NewSprite):
                self.changeSpriteLayer(sprite, 0)
                self._spriteOldRect[sprite] = 0
                self._spriteOldRect[sprite] = sprite.rect
            else:
                self.add(*sprite)
                
#------------------------------------------------------------------------------
            
    def remove(self, sprite, removeEmptyLayers=True):
        """
        Revmoves a sprite from renderer.
        """
        if self._sprites.has_key(sprite):
            self._layers[ self._sprites[sprite] ].remove(sprite)
            if len( self._layers[ self._sprites[sprite] ] )==0 and removeEmptyLayers: # if layer is empty
                del self._layers[ self._sprites[sprite] ]       # remove it
                self._needSortLayers = True
            del self._sprites[sprite]
            del self._spriteOldRect[sprite]
            self._appendDirtyRect(sprite.rect)
            
#------------------------------------------------------------------------------
            
    def changeSpriteLayer(self, sprite, toLayer, removeEmptyLayers=True):
        """
        Changes the layer of a sprite and removes empty layers. Set "removeEmptyLayers" to
        False if you want to retain empty layers.
        """
        if self._sprites.has_key(sprite): # remove sprite from old layer if exists
            self._layers[ self._sprites[sprite] ].remove(sprite)
            if len( self._layers[ self._sprites[sprite] ] )==0 and removeEmptyLayers: # if layer is empty
                del self._layers[ self._sprites[sprite] ]       # remove it
                self._needSortLayers = True
        self._sprites[sprite] = toLayer # assign new layer to sprite
        if self._layers.has_key(toLayer): # and sprite to layer
            self._layers[toLayer].append(sprite) # if layer already exists then append
        else:
            self._layers[toLayer] = [sprite] # else new list
            self._needSortLayers = True
        if 2 != sprite.dirty:
            sprite.dirty = 1
         
#------------------------------------------------------------------------------
        
    def getLayerOfSprite(self, sprite):
        if self._sprites.has_key(sprite):
            return self._layers[ self._sprites[sprite] ]
        else:
            return None
#------------------------------------------------------------------------------
    def getSpritesOfLayer(self, layerNr):
        if self._layers.has_key(layerNr):
            return list(self._layers[layerNr])
        else:
            return None
#------------------------------------------------------------------------------
    def moveLayer(self, toLayer, removeEmptyLayer=True):
        raise NotImplementedError
    
#------------------------------------------------------------------------------
        
    def clear(self):
        """
        Removes all sprites from renderer!
        """
        for spr in self._sprites:
            self.remove(spr)
        self.render()
        self._sprites.clear()
        self._layers.clear()
        self._needSortLayers = True
        
#------------------------------------------------------------------------------
        
    def setBackground(self, bgdSurface):
        """
        Set the background to use to erase the sprites.
        """
        # TODO: stretch or warning if background has not same size as screen???
        self._background = bgdSurface.convert_alpha()
        
#------------------------------------------------------------------------------
    def getBackground(self):
        """
        Return a copy of background surface in use.
        """
        return self._background.copy()
#------------------------------------------------------------------------------
    def getBackgroundReference(self):
        """
        Returns a reference to background surface in use.
        """
        return self._background
#------------------------------------------------------------------------------
        
    def setBackgroundColor(self, color):
        """
        Set the color of the background. color = (R,G,B)
        """
        self._background_color = color
        if self._background != 0:
            self._background.fill(self._background_color)
            self.flip()
        
#------------------------------------------------------------------------------
        
    def fpsSet(self, n=0):
        self._fps = n
#------------------------------------------------------------------------------
    def fpsGet(self):
        return self._fps
#------------------------------------------------------------------------------
    def notifyScreenChange(self):
        # check if screen has changed
            # screen has changed, check if new screen is valid
            self._screen = self._getScreen()
            if self._screen is None:
                self._initialized = False
                self._init()
            self._appendDirtyRect(self._screen.get_rect())
                
        # screen ok
        
#------------------------------------------------------------------------------
        
    def _init(self):
        if(pygame.display.get_init()):
            if(not self._initialized):
                self._screen = pygame.display.get_surface()
                if self._screen is None:
                    raise TypeError, "Could not get a valid screen surface: pygame.display.get_surface()!!"
                if 0 == self._background:
                    self._background = pygame.Surface(self._screen.get_size(), self._screen.get_flags(), self._screen).convert_alpha()
                    self._background.fill(self._background_color) 
                #self._screen.blit(self._background, (0,0))
                self._appendDirtyRect(self._screen.get_rect())
                self._initialized = True
                self.render = self._render
                self.flip = self._flip
                self.flip()
            else:
                raise UserWarning, "Renderer.init(): you should initialize the renderer only once!"
        else:
            raise ReferenceError , "Renderer.init():you must have pygame.display initialized!!"        
        
#------------------------------------------------------------------------------
        
    def _flipInit(self):
        if(pygame.display.get_init()):
            if(not self._initialized):
                self._screen = pygame.display.get_surface()
                if self._screen is None:
                    raise TypeError, "Could not get a valid screen surface: pygame.display.get_surface()!!"
                if 0 == self._background:
                    self._background = pygame.Surface(self._screen.get_size(), self._screen.get_flags(), self._screen)
                    self._background.fill((255,0,255))
                #self._screen.blit(self._background, (0,0))
                self._appendDirtyRect(self._screen.get_rect())
                self._initialized = True
                self.flip = self._flip
                self._flip()
                self.render = self._render
            else:
                raise UserWarning, "Renderer.init(): you should initialize the renderer only once!"
        else:
            raise ReferenceError , "Renderer.init():you must have pygame.display initialized!!"        
        
#------------------------------------------------------------------------------
    def _appendDirtyRect(self, rect):
        

        _update = self._update
        _unionR = pygame.Rect(rect)
        
        i = _unionR.collidelist(_update)
        while -1<i:
            _unionR.union_ip(_update[i])
            del _update[i]
            i = _unionR.collidelist(_update)
        _update.append(_unionR)
            

#------------------------------------------------------------------------------
    def _render(self):
        
        # speedups
        _blit = self._screen.blit
        _background = self._background 
        _spriteOldRect = self._spriteOldRect
        _update = self._update
        _update_append = self._appendDirtyRect
        # find dirty rects on screen and erase dirty sprites from screen(OLD postition!)
        # old and new position of dirty sprite are dirty rects on screen
        for spr in self._sprites.keys():
            if 0<spr.dirty and -1<self._sprites[spr]: # TODO perhaps better to check concrete value?                
                _update_append(_spriteOldRect[spr])# _oldRect)
                _update_append(spr.rect)
                
        # find sprites colliding with dirty rects and blit them
        
        if self._needSortLayers: # only make a sort if needed
            self._needSortLayers = False
            self._sortedLayers = self._layers.keys()
            self._sortedLayers.sort() 
        for r in _update:
            _blit(_background, r, r)
            
        for layer in self._sortedLayers:
            if -1<layer: # layer smaller than 0 are not visible
                for spr in self._layers[ layer ]:
                    
                    if 0==spr.dirty:
                        _spr_rect = spr.rect
                        for idx in _spr_rect.collidelistall(_update):
                        # clip
                            
                            self._screen.set_clip(_update[idx])
                            _blit(spr.image, _spr_rect)
                            self._screen.set_clip()
                        
                    else:
                        _spriteOldRect[spr] = _blit(spr.image, spr.rect)#_blit(spr.image, spr.rect)
                        if 1 == spr.dirty:
                            spr.dirty = 0
                    
        self._display_update(_update)
        self._clock_tick(self._fps)
        _update[:] = []

#------------------------------------------------------------------------------
            
    def _DEBUGrender(self):
        print "DEBUGrender"
        
        # speedups
        _blit = self._screen.blit
        _background = self._background 
        _spriteOldRect = self._spriteOldRect
        _update = self._update
        _update[:] = [] # [pygame.Rect(100,100,200,200)]
        _update_append = self._appendDirtyRect
        # debugging vars
        _debugLostRects = [] # lost rects
        _debugLostRects_append = _debugLostRects.append
        _colorLostR = (255,255,0) # yellow
        _debugOldRects = [] # old position
        _debugOldRects_append = _debugOldRects.append
        _colorOldR = (255,216,66) # orange
        _debugNewRects = [] # new position
        _debugNewRects_append = _debugNewRects.append
        _colorNewR = (0,255,0) # green
        _debugCollidingRects = [] # colliding
        _debugCollidingRects_append = _debugCollidingRects.append
        _colorCollidingR = (0,255,255) # blue
        # debugging vars_update   # update rects
        _colorUpdateR = (255,0,0,128) # red
        _optimizedUpdate = []
        _optimizedUpdate_append = _optimizedUpdate.append
        
        # blit lost rects with background, they are dirty rects on screen
        _oldRect = 0
        _optimized = []
        
        _blit(self._background, (0,0)) # rebuild entire screen (slow!)
        for layer in self._sortedLayers:
            for spr in self._layers[layer]:
                    _blit(spr.image, spr)
        
        
        # find dirty rects on screen and erase dirty sprites from screen(OLD postition!)
        # old and new position of dirty sprite are dirty rects on screen
        for spr in self._sprites.keys():
            _oldRect = _spriteOldRect[spr]
            if 0<spr.dirty and 0!=_oldRect:
                    
                _update_append( _oldRect )
                _update_append(spr.rect)
                
                _debugOldRects_append(_oldRect)
                _debugNewRects_append(spr.rect)
                    
##        print "update: ", _update, "\n"
        # find sprites colliding with dirty rects and blit them
        if self._needSortLayers: # only make a sort if needed
            self._sortedLayers = self._layers.keys()
            self._sortedLayers.sort() 
            self._needSortLayers = False
        for layer in self._sortedLayers:#self._layers.keys().sort():
            if -1<layer:
                for spr in self._layers[ layer ]:
                    if 0==spr.dirty:
                        _spr_rect = spr.rect
                        for idx in _spr_rect.collidelistall(_update):
                        # clip
                            clip = _spr_rect.clip(_update[idx])
##                            _debugCollidingRects_append( _blit(spr.image, clip, pygame.Rect(clip[0]-_spr_rect[0], clip[1]-_spr_rect[1], clip[2], clip[3])) )
                            _debugCollidingRects_append(clip)
                    else:
                        _spriteOldRect[spr] = spr.rect #_blit(spr.image, spr.rect)#_blit(spr.image, spr.rect)
                        if 1 == spr.dirty:
                            spr.dirty = 0
##        print "update: ", _update, "\n"

                    
##        print "\nupdate: ", _update, "\n"
        #pygame.display.update(_update)
        
        # debugging code
        _oldRectsArea = 0
        _collidingRectsArea = 0
        _newRectsArea = 0
        _updateArea = 0
        for r in _debugOldRects:
            pygame.draw.rect(self._screen, _colorOldR, r, 4) # orange
            _oldRectsArea += r.width*r.height
        for r in _debugCollidingRects:
            pygame.draw.rect(self._screen, _colorCollidingR, r,3) # blue
            _collidingRectsArea += r.width*r.height
        for r in _debugNewRects:
            pygame.draw.rect(self._screen, _colorNewR, r, 2) # green
            _newRectsArea += r.width*r.height
        for r in _update:
            pygame.draw.rect(self._screen, _colorUpdateR, r,1) # red
            _updateArea += r.width*r.height
        _optArea = 0
            
        print "screen area:        \t", self._screen.get_width()*self._screen.get_height()
        print "\n"
        print "old rects ",len(_debugOldRects)," area:     \t", _oldRectsArea
        print "colliding rects ",len(_debugCollidingRects)," area:\t", _collidingRectsArea
        print "\n"
        print "screen update rects ",len(_update)," area: \t", _updateArea
        print "optimized screen update rects ",len(_optimized)," area: \t", _optArea
        print "double blittet area: ", _updateArea-_optArea, " # rects diff: ", len(_update)-len(_optimized)
        print "\n"
        print "total blitet rects ",+len(_debugOldRects)+ \
            len(_debugCollidingRects)," area:  \t", _oldRectsArea+_collidingRectsArea
        print "\n"
        _pygame_sprite_update_area = 0
        for s in self._sprites.keys():
            _pygame_sprite_update_area += s.rect.w*s.rect.h*2
        print "pygame.sprite would have a screen update area between max ", \
            _pygame_sprite_update_area, " and min ", _pygame_sprite_update_area/2
        print "and ",len(self._sprites.keys())*2," rects blitet\n\n"
        
        pygame.display.flip() # update entire screen (slow!)
        self._clock_tick(self._fps)    
        
#------------------------------------------------------------------------------
    def _flip(self):
        _blit = self._screen.blit
        _blit(self._background, (0,0))
        if self._needSortLayers: # only make a sort if needed
            self._needSortLayers = False
            self._sortedLayers = self._layers.keys()
            self._sortedLayers.sort() 
        for layer in self._sortedLayers:
            for spr in self._layers[layer]:
                _blit(spr.image, spr)
        pygame.display.flip()
        self._clock_tick(self._fps)
        
#------------------------------------------------------------------------------
        
#==============================================================================

# if you import this module mor than one time only one instance can exist!
if 0==_Renderer._instance:
    Renderer = _Renderer()