[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
Re: [pygame] sprite engine Top this!
- To: pygame-users@xxxxxxxx
- Subject: Re: [pygame] sprite engine Top this!
- From: DR0ID <dr0id@xxxxxxxxxx>
- Date: Mon, 01 May 2006 17:34:23 +0200
- Delivered-to: archiver@seul.org
- Delivered-to: pygame-users-outgoing@seul.org
- Delivered-to: pygame-users@seul.org
- Delivery-date: Mon, 01 May 2006 11:29:24 -0400
- In-reply-to: <445474E7.5060000@comcast.net>
- References: <445474E7.5060000@comcast.net>
- Reply-to: pygame-users@xxxxxxxx
- Sender: owner-pygame-users@xxxxxxxx
- User-agent: Thunderbird 1.5.0.2 (Windows/20060308)
Hi
I worked up a small example to illustrate the dirtyrect
processing method I mentioned earlier.
If you can make this faster, post your results here!
Yes, I can!
Note that all sprites have an alpha channel that must be maintained.
To make a comparable program it must have:
all drawen surface must have the pygame.SRCALPHA flag.
I use 32 bit for the bitdepth.
10 moving rectangles of 30x30 pixels filled with any color but alpha
= 128
1 moving circle of r=50 ( that a 100x100pixel rectangle) filled with
any color and alpha = 128
3 static rectangles topleft (100,300), size(100,300)
(50,450), size(600,100)
(140,400), size(300,300)
filled with any color and alpha = 128
fps string display on screen at topleft (0,0)
static text "Hit ESC to stop, ALT-ENTER to toggle fullscreen mode."
at topleft (150, 0)
Note: static boxes should not be rendered to the background (faster) and
the text should stay in front of the moving parts!
And do not use psyco ore pyrex, just pygame and python!
Results on my machine:
PentiumM 1.5GHz, 1GB ram, WinXP prof. ~880 fps windowed and more or
less the same in fullscreen.
Another test is to add 100 moving rectangles! (change line 133 in
compare.py)
with 100 moving rectangles: ~106 fps
To run my code start "compare.py" press "h" to get help on the keys
(console).
I have made some changes to my code:
added cliping so now I only blit needed pixels
use the code posted by Brian Fisher to get the dirty screen areas
because its the fastest one ( some more pixel to update has lesser cost
then updating many rects):
# Redraw part of screen
R = rect.clip(self.rect)
i = R.collidelist(self.dirtyrects)
if i > -1:
# Add to existing dirty rectangle
while i > -1:
R.union_ip(self.dirtyrects[i])
del self.dirtyrects[i]
i = R.collidelist(self.dirtyrects)
self.dirtyrects.append(R)
As I said, the code is not finished and can have some bugs. If you see
one, please tell me.
I'm wondering which fps you get on your machines. Please tell me so I
can build a little statistic.
~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)")
stepmodus = False
fullscreen = False
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:
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()
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()