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

Re: [pygame] Re: SDL 1.3 blit speed



On 13.07.2011 07:32, Brian Fisher wrote:
One particular technique for scrolling multi-layer backgrounds (that don't have parallax or animation anyways) is to have one large surface which is big enough to cover the screen, and to treat it as a wrapping buffer (so you do 4 blits to the screen from the surface in order to get the parts of the buffer as they wrap around the edges - hope that makes sense). Then as the background scrolls, you render in the newly visible parts into the buffer from your layers (using source clipping to get just the newly visible portion rendering on top of the newly offscreen part)

It can have dramatic speed improvements compared to redrawing all layers every time because when you have a lot of layers with transparency, all that transparency gets flattened in the cache. The blit to the screen from the buffer is just a copy, it doesn't spend time on color-key or alpha blending (this technique is actually great for when you want alpha blended layers, btw, which can look better than color key).  Also, you'll have fewer blit calls as well, which means fewer native code crossings from python which are moderately slow.



Hi

Out of curiosity, I have implemented a scrolling buffer (see attachment, bottom left is the 'screen', bottom right the 'buffer' and the yellow rect is the camera and the background is the 'world').

I encountered some pitfalls:

  • wrapping isn't that easy (need to detect wrapping and fill the buffer accordingly)
  • scrolling diagonally can be decomposed into scrolling in each axis, but need to use the old values for the first axis you update (e.g. I updated the x-axis first, but needed to use the old ypos of the camera not the new one)
  • if the scrolling is more than the width or height of the buffer then you should refill the entire buffer
  • direction of scrolling changes the positions of the areas that need to be updated
  • need of an interface to the world to get the rendered portions into the buffer

Therefore I divided the problem in cases:

  1. no scroll (buffer stays as it is)
  2. scrolling without wrapping
  3. wrapping
  4. scrolling distance is grater than either the height or width of the buffer (need to refill entire buffer)
The implementation is more a prototype or a proof of concept and is not optimized. Also the draw operations are not separated from the update of the buffer internals (is there an elegant way to do that?).

If someone has a another (different/better/simpler/faster) way to implement such a scrolling buffer, I would definitively be interested to see that implementation.

Especially the interface to the world would be interesting. Any suggestions are welcome.

As pointed out, this type of scrolling buffer works only with static layers. I'm not completely sure of its benefit since you need to blit the 4 parts (~ equals 1 full screen blit) to screen each frame. The rendering of the world would need to be very expensive to get most benefit from it.

Maybe a simpler solution using a buffer that is a bit bigger as the screen and use the scroll method of the Surface class and then refilling the scroll delta might be faster, but I have neither tested nor profiled it (nor do I have an implementation for that idea). You still need to blit the entire screen and I'm not sure how the scroll method is implemented (since actually one could do a blit on the same surface, but this would mean that you do two fill screen blits each frame).

Any suggestions welcome.

Thanks.

~DR0ID
# -*- coding: utf-8 -*-

"""
Scroll buffer prototypes.

"""

import sys

import pygame

# #  ----------------------------------------------------------------------------

# class ScrollBuffer1D(object):

    # def __init__(self, width, height):
        # self._buffer = pygame.Surface((width, height))
        # self._post_x = sys.maxint
        # self._post_y = sys.maxint
        # self._cam = pygame.Rect(sys.maxint, sys.maxint, width, height)

    # def scroll_to(self, xpos, ypos, world):
        # dx = xpos - self._cam.left
        # if dx > 0:
            # if dx > self._cam.width:
                # self._refill(xpos, ypos, world)
            # else:
                # area = pygame.Rect(self._cam.right, ypos, dx, self._cam.height)
                # surf = world.get_render(area)
                # self._buffer.blit(surf, (self._post_x, 0))
                # # this would require to clip the subsurface rect to the buffer surface size
                # # world.draw(self._buffer.subsurface(pygame.Rect(self._post_x, 0, dx, self._cam.height)), area)
                # self._post_x += dx
                # if self._post_x > self._cam.width:
                    # self._post_x -= self._cam.width
                    # self._buffer.blit(surf, (self._post_x - dx, 0))
        # elif dx < 0:
            # if dx < -self._cam.width:
                # self._refill(xpos, ypos, world)
            # else:
                # area = pygame.Rect(self._cam.left, ypos, dx, self._cam.height)
                # area.normalize()
                # surf = world.get_render(area)
                # self._post_x += dx
                # self._buffer.blit(surf, (self._post_x, 0))
                # if self._post_x < 0:
                    # self._post_x += self._cam.width
                    # self._buffer.blit(surf, (self._post_x, 0))
        # self._cam.left = xpos

    # def _refill(self, xpos, ypos, world):
        # self._cam.topleft = xpos, ypos
        # surf = world.get_render(self._cam)
        # self._post_x = xpos % self._cam.width
        # self._buffer.blit(surf, (0, 0), pygame.Rect(self._cam.width - self._post_x, 0, self._post_x, self._cam.height))
        # self._buffer.blit(surf, (self._post_x, 0), pygame.Rect(0, 0, self._cam.width - self._post_x, self._cam.height))
        
    # def draw(self, screen):
        # source_left = pygame.Rect(self._post_x, 0, self._cam.width - self._post_x, self._cam.height)
        # screen.blit(self._buffer, (0, 0), source_left)
        
        # source_right =  pygame.Rect(0, 0, self._post_x, self._cam.height)
        # screen.blit(self._buffer, (self._cam.width - self._post_x, 0), source_right)
        
        # # pygame.draw.rect(screen, (255, 0, 0), source_left, 1)
        # # pygame.draw.rect(screen, (0, 255, 0), source_right, 1)
        
#  ----------------------------------------------------------------------------

class ScrollBuffer2D(object):

    # +--------+------+-      +------+--------+       
    # |   1    |  2   |       |  4   |   3    |       
    # |        |      |       +------+--------+       
    # +--------+------+-      |  2   |   1    |       
    # |   3    |  4   |       |      |        |       
    # +--------+------+-      +------+--------+       
    # Screen                  Buffer


    def __init__(self, width, height):
        """
            width and height of the buffer
        """
        self._buffer = pygame.Surface((width, height))
        self._post_x = sys.maxint
        self._post_y = sys.maxint
        self._cam = pygame.Rect(sys.maxint, sys.maxint, width, height)
        
    def scroll_to(self, xpos, ypos, world):
        """
            Scroll to the world position xpos, ypos
        
            world should have a method called  get_render(rect), where rect is
            an area of the world in world-coordinates. It should return a 
            surface of the same size as the rect.
        """
        # x-axis first, need to use self._cam.top (old value) instead of ypos (new value)
        dx = xpos - self._cam.left
        if dx > 0:
            # scroll in positive x-axis direction
            if dx > self._cam.width:
                # refill entire buffer
                self._refill(xpos, ypos, world)
            else:
                area = pygame.Rect(self._cam.right, self._cam.top, dx, self._cam.height)
                surf = world.get_render(area)
                # extend buffer rect 4
                self._buffer.blit(surf, (self._post_x, 0), (0, self._cam.height - self._post_y, dx, self._post_y))
                # extend buffer rect 2
                self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, dx, self._cam.height - self._post_y))
                self._post_x += dx
                # check for wrapping
                if self._post_x > self._cam.width:
                    self._post_x -= self._cam.width
                    # extending buffer rect 4
                    self._buffer.blit(surf, (self._post_x - dx, 0), (0, self._cam.height - self._post_y, dx, self._post_y))
                    # extending buffer rect 2
                    self._buffer.blit(surf, (self._post_x - dx, self._post_y), (0, 0, dx, self._cam.height - self._post_y))
        elif dx < 0:
            # scroll in negative x-axis direction
            if dx < -self._cam.width:
                # refill entire buffer
                self._refill(xpos, ypos, world)
            else:
                area = pygame.Rect(self._cam.left, self._cam.top, dx, self._cam.height)
                area.normalize()
                surf = world.get_render(area)
                self._post_x += dx
                # extend buffer rect 3
                self._buffer.blit(surf, (self._post_x, 0), (0, self._cam.height - self._post_y, dx, self._post_y))
                # extend buffer rect 1
                self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, dx, self._cam.height - self._post_y))
                
                # check for wrapping
                if self._post_x < 0:
                    self._post_x += self._cam.width
                    # rect 4
                    self._buffer.blit(surf, (self._post_x, 0), (0, self._cam.height - self._post_y, dx, self._post_y))
                    # rect 2
                    self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, dx, self._cam.height - self._post_y))
        self._cam.left = xpos
        
        # y-axis
        dy = ypos - self._cam.top
        if dy > 0:
            if dy > self._cam.height:
                self._refill(xpos, ypos, world)
            else:
                # scroll positive y direction
                area = pygame.Rect(xpos, self._cam.bottom, self._cam.width, dy)
                surf = world.get_render(area)
                # extend buffer rect 4
                self._buffer.blit(surf, (0, self._post_y), (self._cam.width - self._post_x, 0, self._post_x, dy))
                # extend buffer rect 3
                self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, self._cam.width - self._post_x, dy))
                self._post_y += dy
                # check for wrapping
                if self._post_y > self._cam.height:
                    self._post_y -= self._cam.height
                    # extend buffer rect 4
                    self._buffer.blit(surf, (0, self._post_y - dy), (self._cam.width - self._post_x, 0, self._post_x, dy))
                    # extend buffer rect 3
                    self._buffer.blit(surf, (self._post_x, self._post_y - dy), (0, 0, self._cam.width - self._post_x, dy))
        elif dy < 0:
            if dy < -self._cam.height:
                self._refill(xpos, ypos, world)
            else:
                # scroll negative y direction
                area = pygame.Rect(xpos, self._cam.top, self._cam.width, dy)
                area.normalize()
                surf = world.get_render(area)
                self._post_y += dy
                # extend buffer rect 2
                self._buffer.blit(surf, (0, self._post_y), (self._cam.width - self._post_x, 0, self._post_x, -dy))
                # extend buffer rect 1
                self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, self._cam.width - self._post_x, -dy))
                # check for wrapping
                if self._post_y < 0:
                    self._post_y += self._cam.height
                    # extend buffer rect 2
                    self._buffer.blit(surf, (0, self._post_y), (self._cam.width - self._post_x, 0, self._post_x, -dy))
                    # extend buffer rect 1
                    self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, self._cam.width - self._post_x, -dy))
        self._cam.top = ypos
        
    def _refill(self, xpos, ypos, world):
        """ Refills the entire buffer"""
        self._cam.topleft = xpos, ypos
        surf = world.get_render(self._cam)
        self._post_x = xpos % self._cam.width
        self._post_y = ypos % self._cam.height
        # +--------+------+-      +------+--------+       
        # |   1    |  2   |       |  4   |   3    |       
        # |        |      |       +------+--------+       
        # +--------+------+-      |  2   |   1    |       
        # |   3    |  4   |       |      |        |       
        # +--------+------+-      +------+--------+       
        # Screen                  Buffer
        screen_post_x = self._cam.width - self._post_x
        screen_post_y = self._cam.height - self._post_y
        self._buffer.blit(surf, (0, 0), (screen_post_x, screen_post_y, self._post_x, self._post_y))
        self._buffer.blit(surf, (self._post_x, 0), (0, screen_post_y, screen_post_x, self._post_y))
        self._buffer.blit(surf, (0, self._post_y), (screen_post_x, 0, self._post_x, screen_post_y))
        self._buffer.blit(surf, (self._post_x, self._post_y), (0, 0, screen_post_x, screen_post_y))
        
    def draw(self, screen):
        """ Draw the buffer to the screen"""
        # +--------+------+-      +------+--------+       
        # |   1    |  2   |       |  4   |   3    |       
        # |        |      |       +------+--------+       
        # +--------+------+-      |  2   |   1    |       
        # |   3    |  4   |       |      |        |       
        # +--------+------+-      +------+--------+       
        # Screen                  Buffer
        screen_post_x = self._cam.width - self._post_x
        screen_post_y = self._cam.height - self._post_y
        screen.blit(self._buffer, (0, 0), (self._post_x, self._post_y, screen_post_x, screen_post_y))
        screen.blit(self._buffer, (screen_post_x, 0), (0, self._post_y, self._post_x, screen_post_y))
        screen.blit(self._buffer, (0, screen_post_y), (self._post_x, 0, screen_post_x, self._post_y))
        screen.blit(self._buffer, (screen_post_x, screen_post_y), (0, 0, self._post_x, self._post_y))

#  ----------------------------------------------------------------------------


class World(object):
    """
        Simple world for testing
    """
    
    def get_render(self, rect): 
        return pygame.display.get_surface().subsurface(rect)
    def draw(self, surf, world_area):
        surf.blit(pygame.display.get_surface(), (0, 0), world_area)
        
#  ----------------------------------------------------------------------------
def main():
    size = (800, 600)
    pygame.init()
    
    sb = ScrollBuffer2D(200, 100)
    world = World()
    
    # world position of the camera, the position to scroll to
    xpos = 0
    ypos = 0
    
    
    screen = pygame.display.set_mode(size)
    # our test screen is only 200x100 pixels
    pscr = screen.subsurface(pygame.Rect(0, 500, 200, 100))
    
    # this is the rendered 'world'
    background = pygame.Surface(size)
    import random
    ri = random.randint
    for i in range(100):
        color = (ri(0, 255), ri(0, 255), ri(0, 255))
        pygame.draw.line(background, color, (ri(0, 800), ri(0, 400)), (ri(0, 800), ri(0, 400)), ri(1, 5))

        
    running = True
    while running:
        
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False
            elif e.type == pygame.KEYDOWN:
                if e.key == pygame.K_ESCAPE:
                    running = False
                elif e.key == pygame.K_LEFT:
                    xpos -= 15
                elif e.key == pygame.K_RIGHT:
                    xpos += 15
                elif e.key == pygame.K_UP:
                    ypos -= 10
                elif e.key == pygame.K_DOWN:
                    ypos += 10
                elif e.key == pygame.K_j:
                    xpos -= 300
                elif e.key == pygame.K_l:
                    xpos += 300
                elif e.key == pygame.K_i:
                    ypos -= 300
                elif e.key == pygame.K_k:
                    ypos += 300
                elif e.key == pygame.K_a:
                    xpos -= 30
                    ypos -= 30
                elif e.key == pygame.K_d:
                    xpos += 30
                    ypos += 30

        screen.fill((0, 0, 0))
        screen.blit(background, (0, 0))
                    
        # update the scroll position
        sb.scroll_to(xpos, ypos, world)
        # draw the buffer to screen
        sb.draw(pscr)
        
        # this are auxilarity graphics
        # draw a yellow rect as camera in the world
        pygame.draw.rect(screen, (255, 255, 0), (xpos, ypos, 200, 100), 1)
        # show the buffer internals
        screen.blit(sb._buffer, (600, 500))
                    
        # draw a red rect around the 'scree' (bottom left)
        pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(0, 500, 200, 100), 2)
        # draw a red rect around the buffers internal (bottom right)
        pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(600, 500, 200, 100), 2)
        # draw the wrap lines of the buffer
        pygame.draw.line(screen, (255, 255, 0), (600 + sb._post_x, 500), (600 + sb._post_x, 600), 1)
        pygame.draw.line(screen, (255, 255, 0), (600, 500 + sb._post_y), (800, 500 + sb._post_y), 1)
        
        pygame.display.flip()

if __name__ == "__main__":
    main()