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()