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