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) 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:
Therefore I divided the problem in cases:
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()