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

Re: [pygame] Dirty Rectangle Processing



Hey Kamilche

On 4/30/06, Kamilche <klachemin@xxxxxxxxxxx> wrote:
I worked up a small example to illustrate the dirtyrect
processing method I mentioned earlier.

It's very nice and clean. simple and easy to read. veryg ood

I don't think the sample shows off where I think your approach really
shines over traditional dirty rect stuff though - which is when you
have stuff that isn't animating interleaved with stuff that is, and
the stuff that isn't animating doesn't need to redraw - which means
you can have a bunch of sprites sitting there to build your
environment and they only draw exactly as much as needed. neat.

also, there was a little quirk in the way you were adding new dirty
rects, in that a rect may intersect with multiple existing rects, or
may only intersect with existing rects after it has combined with
others. What this means is that with a  lot of overlapping dirty
objects, if you combine it with one rect only, you will end up with a
lot of overlapping dirty rects. Since your code will draw each object
for each rect it intersects, having overlapping dirty rects can mean a
moderate amount of extra drawing of the same objects. You can "fix"
this quirk by making new rects continue to combine with existing ones
until it's one big rect.

so I'm attaching a modified version of your sample that I think
addresses the stuff I mentioned. you can press r to toggle drawing of
dirty rects (yes the screen gets ugly quickly - that's because it's
designed to show you the rects with as little performance hit as
possible, and stuff that was dirty for the update won't be cleared
next frame, so there ya go) but you can use that to see that the 3
static rects don't redraw and see the quirk I mentioned. You can press
d to toggle whether the aggregation is multi-pass or not on nw rects.
and theres a bunch more rects to make the aggregation thing more
obvious.


'''

Pygame Dirtyrect Processing

An example of dirtyrect processing with Pygame.
Make it faster, if you can!  My speed:
604 FPS on a 2.6 gHz Windows 2000 box w/1 gig memory.
'''


import pygame
import random
import time
import os
import sys

screen = None
sprites = []

class Sprite(object):
    pos = (0, 0)
    size = (30, 30)
    rect = None
    paint = 0
    velocity = (2, 1)

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        self.rect = pygame.Rect((0, 0, 0, 0))
        if type(self.pos) != list:
            self.pos = [random.randint(0, screen.size[0]-1),
                        random.randint(0, screen.size[1]-1)]
        self.size = list(self.size)
        self.velocity = list(self.velocity)
        sprites.append(self)
        self.Paint()

    def Paint(self):
        raise Exception("You must override the paint method!")

    def Repaint(self):
        self.paint = 1
        screen.Dirty(self.rect)

    def Draw(self, bg, dirtyrect):
        ' Draw the sprite'
        # Repaint if necessary
        if self.paint:
            self.Paint()
            self.paint = 0

        # Clip picture against the dirtyrect
        r = self.rect.clip(dirtyrect)
        if r:
            # Paint the clipped rect only
            sourcerect = pygame.Rect(r[0]-self.rect[0], r[1]-self.rect[1], r.w, r.h)
            bg.blit(self.pic, r, sourcerect)

    def Move(self):
        ' Move the sprite'
        newx = self.pos[0] + self.velocity[0]
        newy = self.pos[1] + self.velocity[1]
        if 0 <= newx <= screen.size[0]:
            self.pos[0] = newx
            self.SetRect() # Not paint, because pic didn't change
        else:
            self.velocity[0] = -self.velocity[0]
        if 0 <= newy <= screen.size[1]:
            self.pos[1] = newy
            self.SetRect() # Not paint, because pic didn't change
        else:
            self.velocity[1] = -self.velocity[1]

    def Timer(self, now):
        self.Move()

    def SetRect(self):
        " Dirty this item's current location, set its rectangle based on its"
        " current size and position, and dirty it again."
        if self.size[0] == 0 or self.size[1] == 0:
            return
        screen.Dirty(self.rect)
        self.rect.topleft = self.pos
        self.rect.size = self.size
        screen.Dirty(self.rect)

class StaticSquare(Sprite):

    def Move(self):
        pass
        
    def Paint(self):
        self.pic = pygame.Surface(self.size, pygame.SRCALPHA, 32).convert_alpha()
        self.pic.fill(self.backcolor)
        self.SetRect()

class Square(Sprite):

    def Paint(self):
        self.pic = pygame.Surface(self.size, pygame.SRCALPHA, 32).convert_alpha()
        self.pic.fill(self.backcolor)
        self.SetRect()


class Circle(Sprite):

    def Paint(self):
        self.pic = pygame.Surface(self.size, pygame.SRCALPHA, 32).convert_alpha()
        self.pic.fill((0, 0, 0, 0))
        radius = self.size[1]/2
        center = self.size[0]/2, self.size[1]/2
        pygame.draw.circle(self.pic, self.backcolor, center, radius, 0)
        self.SetRect()


class Text(Sprite):
    text = ''
    fontcolor = (255, 255, 255)
    font = None

    def __init__(self, **kwargs):
        if Text.font == None:
           Text.font = pygame.font.Font(pygame.font.get_default_font(), 18)
        self.SetText(self.text)
        super(Text, self).__init__(**kwargs)

    def Paint(self):
        self.pic = self.font.render(self.text, 1, self.fontcolor).convert_alpha()
        self.size = self.pic.get_size()
        self.SetRect()

    def SetText(self, text):
        self.text = text
        self.Repaint()

    def Timer(self, now):
        pass


class FPS(Text):
    nexttime = 0
    ctr = 0

    def Timer(self, now):
        self.ctr += 1
        if now > self.nexttime:
            self.SetText("%d FPS" % self.ctr)
            self.ctr = 0
            self.nexttime = now + 1


class Screen(object):
    size = [800, 600]
    pos = [0, 0]
    dirtyrects = []
    backcolor = (0, 64, 0)
    fullscreen = 0
    bitdepth = 16
    def __init__(self):
        self.CreateScreen()

    def DirtyOriginal(self, rect):
        " Mark a rectangle on the screen as needing to be redrawn ('Dirty' it)"
        if rect == None:
            # Redraw entire screen
            del self.dirtyrects[:]
            self.dirtyrects = [self.rect]
        else:
            # Redraw part of screen
            R = rect.clip(self.rect)
            i = R.collidelist(self.dirtyrects)
            if i > -1:
                # Add to existing dirty rectangle
                R.union_ip(self.dirtyrects[i])
                self.dirtyrects[i] = (R)
            else:
                # Add a new dirty rectangle
                self.dirtyrects.append(R)

    def DirtyMultiPass(self, rect):
        " Mark a rectangle on the screen as needing to be redrawn ('Dirty' it)"
        if rect == None:
            # Redraw entire screen
            del self.dirtyrects[:]
            self.dirtyrects = [self.rect]
        else:
            # 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)
            else:
                # Add a new dirty rectangle
                self.dirtyrects.append(R)

    Dirty = DirtyOriginal
    
    def Draw(self, draw_rects = False):
        ' Main render routine'
        while len(self.dirtyrects) > 0:
            r = self.dirtyrects[0]
            del self.dirtyrects[0]
            self.screen.fill(self.backcolor, r)
            for sprite in sprites:
                sprite.Draw(self.screen, r)
            if draw_rects:
                cached = pygame.Surface([r.width, r.height], 0, self.screen)
                cached.blit(self.screen, [0,0], r)
                pygame.draw.rect(self.screen, (255,255,255,192), r, 1)
            pygame.display.update(r)
            if draw_rects:
                self.screen.blit(cached, r)

    def ToggleFullscreen(self):
        ' Toggle fullscreen mode'
        if self.fullscreen:
            self.fullscreen = 0
        else:
            self.fullscreen = 1
        self.CreateScreen()

    def CreateScreen(self):
        ' Create the main display screen'
        if self.fullscreen:
            flags = pygame.FULLSCREEN
        else:
            flags = 0
        minbitdepth = 16
        bitdepth = pygame.display.mode_ok(self.size, flags, self.bitdepth)
        if bitdepth < minbitdepth:
            raise Exception("Your system is unable to display %d bit color in an %d x %d window!" % (minbitdepth, self.size[0], self.size[1]))
        self.screen = pygame.display.set_mode(self.size, flags, bitdepth)
        self.rect = pygame.Rect(0, 0, self.size[0], self.size[1])
        self.Dirty(None)


def main():
    global screen
    pygame.init()
    colors = [(255,   0,   0, 255),
              (  0, 255,   0, 255),
              (  0,   0, 255, 255),
              (255, 255,   0, 255),
              (0,   255, 255, 255),
              (255,   0, 255, 255),
              (255, 128,   0, 255),
              (0,   255, 128, 255),
              (128,   0, 255, 255),
              (255, 255, 255, 255)]
    screen = Screen()
    StaticSquare(pos=[100,300],backcolor = (160, 160, 0, 128), size = [100, 300])
    for i in range(80):
        Square(backcolor = colors[i % 10])
    StaticSquare(pos=[50,450],backcolor = (160, 0, 160, 128), size = [600, 100])
    Circle(backcolor = (255, 255, 0, 255), size = [100, 100])
    StaticSquare(pos=[140,400],backcolor = (0, 160, 160, 128), size = [300, 300])
    FPS(pos = [0, 0])
    Text(pos = [150, 0], text = 'ESC to stop, ALT-ENTER fullscreen, r to draw rects, d toggles aggregation')
    quit = 0
    draw_rects = False
    while not quit:
        now = time.time()
        for sprite in sprites:
            sprite.Timer(now)
        screen.Draw(draw_rects)
        pygame.event.pump()
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                quit = 1
            elif e.type == pygame.KEYDOWN:
                if e.key == pygame.K_r:
                    draw_rects = not draw_rects
                    screen.Dirty(None)
                if e.key == pygame.K_d:
                    if Screen.Dirty == Screen.DirtyOriginal:
                        Screen.Dirty = Screen.DirtyMultiPass
                    else:
                        Screen.Dirty = Screen.DirtyOriginal
                if e.key == pygame.K_ESCAPE:
                    quit = 1
                elif e.key == pygame.K_RETURN:
                    if pygame.key.get_mods() & pygame.KMOD_ALT:
                        screen.ToggleFullscreen()
    pygame.quit()

if sys.platform == 'win32':
    os.environ['SDL_VIDEO_WINDOW_POS'] = '3,23'
    os.environ['SDL_VIDEODRIVER'] = 'windib'

main()