Ok so I included the code for my project below, I'm just doing some experimenting with pygame on making a platformer. I'm trying to figure out how to do some very simple scrolling that follows the player, so the player is the center of the camera and it bounces/follows him. Can anyone help me?
import pygame from pygame import * WIN_WIDTH = 800 WIN_HEIGHT = 640 HALF_WIDTH = int(WIN_WIDTH / 2) HALF_HEIGHT = int(WIN_HEIGHT / 2) DISPLAY = (WIN_WIDTH, WIN_HEIGHT) DEPTH = 32 FLAGS = 0 CAMERA_SLACK = 30 def main(): global cameraX, cameraY pygame.init() screen = pygame.display.set_mode(DISPLAY, FLAGS, DEPTH) pygame.display.set_caption("Use arrows to move!") timer = pygame.time.Clock() up = down = left = right = running = False bg = Surface((32,32)) bg.convert() bg.fill(Color("#000000")) entities = pygame.sprite.Group() player = Player(32, 32) platforms =  x = y = 0 level = [ "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", "P P", "P P", "P P", "P P", "P P", "P P", "P P", "P PPPPPPPPPPP P", "P P", "P P", "P P", "P P", "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",] # build the level for row in level: for col in row: if col == "P": p = Platform(x, y) platforms.append(p) entities.add(p) if col == "E": e = ExitBlock(x, y) platforms.append(e) entities.add(e) x += 32 y += 32 x = 0 entities.add(player) while 1: timer.tick(60) for e in pygame.event.get(): if e.type == QUIT: raise SystemExit, "QUIT" if e.type == KEYDOWN and e.key == K_ESCAPE: raise SystemExit, "ESCAPE" if e.type == KEYDOWN and e.key == K_UP: up = True if e.type == KEYDOWN and e.key == K_DOWN: down = True if e.type == KEYDOWN and e.key == K_LEFT: left = True if e.type == KEYDOWN and e.key == K_RIGHT: right = True if e.type == KEYDOWN and e.key == K_SPACE: running = True if e.type == KEYUP and e.key == K_UP: up = False if e.type == KEYUP and e.key == K_DOWN: down = False if e.type == KEYUP and e.key == K_RIGHT: right = False if e.type == KEYUP and e.key == K_LEFT: left = False if e.type == KEYUP and e.key == K_RIGHT: right = False # draw background for y in range(32): for x in range(32): screen.blit(bg, (x * 32, y * 32)) # update player, draw everything else player.update(up, down, left, right, running, platforms) entities.draw(screen) pygame.display.update() class Entity(pygame.sprite.Sprite): def __init__(self): pygame.sprite.Sprite.__init__(self) class Player(Entity): def __init__(self, x, y): Entity.__init__(self) self.xvel = 0 self.yvel = 0 self.onGround = False self.image = Surface((32,32)) self.image.fill(Color("#0000FF")) self.image.convert() self.rect = Rect(x, y, 32, 32) def update(self, up, down, left, right, running, platforms): if up: # only jump if on the ground if self.onGround: self.yvel -= 10 if down: pass if running: self.xvel = 12 if left: self.xvel = -8 if right: self.xvel = 8 if not self.onGround: # only accelerate with gravity if in the air self.yvel += 0.3 # max falling speed if self.yvel > 100: self.yvel = 100 if not(left or right): self.xvel = 0 # increment in x direction self.rect.left += self.xvel # do x-axis collisions self.collide(self.xvel, 0, platforms) # increment in y direction self.rect.top += self.yvel # assuming we're in the air self.onGround = False; # do y-axis collisions self.collide(0, self.yvel, platforms) def collide(self, xvel, yvel, platforms): for p in platforms: if pygame.sprite.collide_rect(self, p): if isinstance(p, ExitBlock): pygame.event.post(pygame.event.Event(QUIT)) if xvel > 0: self.rect.right = p.rect.left print "collide right" if xvel < 0: self.rect.left = p.rect.right print "collide left" if yvel > 0: self.rect.bottom = p.rect.top self.onGround = True self.yvel = 0 if yvel < 0: self.rect.top = p.rect.bottom class Platform(Entity): def __init__(self, x, y): Entity.__init__(self) self.image = Surface((32, 32)) self.image.convert() self.image.fill(Color("#DDDDDD")) self.rect = Rect(x, y, 32, 32) def update(self): pass class ExitBlock(Platform): def __init__(self, x, y): Platform.__init__(self, x, y) self.image.fill(Color("#0033FF")) if __name__ == "__main__": main()
You need to apply an offset to the position of your entities when drawing them. Let's call that offset a
camera, since this is the effect we want to achieve with this.
First of all, we can't use the
draw function of the sprite group, since the sprites don't need to know that their position (
rect) is not the position they are going to be drawn on the screen (At the end, we'll subclass the
Group class and reimplement the it's
draw to be aware of the camera, but let's start slow).
Let's start by creating a
Camera class to hold the state of the offset we want to apply to the position of our entities:
class Camera(object): def __init__(self, camera_func, width, height): self.camera_func = camera_func self.state = Rect(0, 0, width, height) def apply(self, target): return target.rect.move(self.state.topleft) def update(self, target): self.state = self.camera_func(self.state, target.rect)
some things to note here:
We need to store the position of the camera, and the width and height of the level in pixels (since we want to stop scrolling at the edges of the level). I used a
Rect to store all these informations, but you could easily just use some fields.
Rect comes in handy in the
apply function. This is where we re-calculate the position of an entity on the screen to apply the scrolling.
Once per iteration of the main loop, we need to update the position of the camera, hence there's the
update function. It just alters the state by calling the
camera_func function, which will do all the hard work for us. We implement it later.
Let's create an instace of the camera:
for row in level: ... total_level_width = len(level)*32 # calculate size of level in pixels total_level_height = len(level)*32 # maybe make 32 an constant camera = Camera(*to_be_implemented*, total_level_width, total_level_height) entities.add(player)
and alter our main loop:
# draw background for y in range(32): ... camera.update(player) # camera follows player. Note that we could also follow any other sprite # update player, draw everything else player.update(up, down, left, right, running, platforms) for e in entities: # apply the offset to each entity. # call this for everything that should scroll, # which is basically everything other than GUI/HUD/UI screen.blit(e.image, camera.apply(e)) pygame.display.update()
Our camera class is already very flexible and yet dead simple. It can use different kinds of scrolling (by providing different
camera_func functions), and it can follow any arbitary sprite, not just the player. You even can change this at runtime.
Now for the implementation of
camera_func. A simple approach is to just center the player (or whichever entity we want to follow) at the screen, and the implementation is straight forward:
def simple_camera(camera, target_rect): l, t, _, _ = target_rect # l = left, t = top _, _, w, h = camera # w = width, h = height return Rect(-l+HALF_WIDTH, -t+HALF_HEIGHT, w, h)
We just take the position of our
target, and add the half total screen size. You can try it by creating your camera like this:
camera = Camera(simple_camera, total_level_width, total_level_height)
So far, so good. But maybe we don't want to see the black background outside the level? How about:
def complex_camera(camera, target_rect): # we want to center target_rect x = -target_rect.center + WIN_WIDTH/2 y = -target_rect.center + WIN_HEIGHT/2 # move the camera. Let's use some vectors so we can easily substract/multiply camera.topleft += (pygame.Vector2((x, y)) - pygame.Vector2(camera.topleft)) * 0.06 # add some smoothness coolnes # set max/min x/y so we don't see stuff outside the world camera.x = max(-(camera.width-WIN_WIDTH), min(0, camera.x)) camera.y = max(-(camera.height-WIN_HEIGHT), min(0, camera.y)) return camera
Here we simply use the
max functions to ensure we don't scroll outside out level.
Try it by creating your camera like this:
camera = Camera(complex_camera, total_level_width, total_level_height)
There's a little animation of our final scrolling in action:
Here's the complete code again. Note I changed some things:
- the level is bigger and to have some more platforms
- use python 3
- use a sprite group to handle the camera
- refactored some duplicate code
- since Vector2/3 is now stable, use them for easier math
- get rid of that ugly event handling code and use
#! /usr/bin/python import pygame from pygame import * SCREEN_SIZE = pygame.Rect((0, 0, 800, 640)) TILE_SIZE = 32 GRAVITY = pygame.Vector2((0, 0.3)) class CameraAwareLayeredUpdates(pygame.sprite.LayeredUpdates): def __init__(self, target, world_size): super().__init__() self.target = target self.cam = pygame.Vector2(0, 0) self.world_size = world_size if self.target: self.add(target) def update(self, *args): super().update(*args) if self.target: x = -self.target.rect.center + SCREEN_SIZE.width/2 y = -self.target.rect.center + SCREEN_SIZE.height/2 self.cam += (pygame.Vector2((x, y)) - self.cam) * 0.05 self.cam.x = max(-(self.world_size.width-SCREEN_SIZE.width), min(0, self.cam.x)) self.cam.y = max(-(self.world_size.height-SCREEN_SIZE.height), min(0, self.cam.y)) def draw(self, surface): spritedict = self.spritedict surface_blit = surface.blit dirty = self.lostsprites self.lostsprites =  dirty_append = dirty.append init_rect = self._init_rect for spr in self.sprites(): rec = spritedict[spr] newrect = surface_blit(spr.image, spr.rect.move(self.cam)) if rec is init_rect: dirty_append(newrect) else: if newrect.colliderect(rec): dirty_append(newrect.union(rec)) else: dirty_append(newrect) dirty_append(rec) spritedict[spr] = newrect return dirty def main(): pygame.init() screen = pygame.display.set_mode(SCREEN_SIZE.size) pygame.display.set_caption("Use arrows to move!") timer = pygame.time.Clock() level = [ "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", "P P", "P P", "P P", "P PPPPPPPPPPP P", "P P", "P P", "P P", "P PPPPPPPP P", "P P", "P PPPPPPP P", "P PPPPPP P", "P P", "P PPPPPPP P", "P P", "P PPPPPP P", "P P", "P PPPPPPPPPPP P", "P P", "P PPPPPPPPPPP P", "P P", "P P", "P P", "P P", "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",] platforms = pygame.sprite.Group() player = Player(platforms, (TILE_SIZE, TILE_SIZE)) level_width = len(level)*TILE_SIZE level_height = len(level)*TILE_SIZE entities = CameraAwareLayeredUpdates(player, pygame.Rect(0, 0, level_width, level_height)) # build the level x = y = 0 for row in level: for col in row: if col == "P": Platform((x, y), platforms, entities) if col == "E": ExitBlock((x, y), platforms, entities) x += TILE_SIZE y += TILE_SIZE x = 0 while 1: for e in pygame.event.get(): if e.type == QUIT: return if e.type == KEYDOWN and e.key == K_ESCAPE: return entities.update() screen.fill((0, 0, 0)) entities.draw(screen) pygame.display.update() timer.tick(60) class Entity(pygame.sprite.Sprite): def __init__(self, color, pos, *groups): super().__init__(*groups) self.image = Surface((TILE_SIZE, TILE_SIZE)) self.image.fill(color) self.rect = self.image.get_rect(topleft=pos) class Player(Entity): def __init__(self, platforms, pos, *groups): super().__init__(Color("#0000FF"), pos) self.vel = pygame.Vector2((0, 0)) self.onGround = False self.platforms = platforms self.speed = 8 self.jump_strength = 10 def update(self): pressed = pygame.key.get_pressed() up = pressed[K_UP] left = pressed[K_LEFT] right = pressed[K_RIGHT] running = pressed[K_SPACE] if up: # only jump if on the ground if self.onGround: self.vel.y = -self.jump_strength if left: self.vel.x = -self.speed if right: self.vel.x = self.speed if running: self.vel.x *= 1.5 if not self.onGround: # only accelerate with gravity if in the air self.vel += GRAVITY # max falling speed if self.vel.y > 100: self.vel.y = 100 print(self.vel.y) if not(left or right): self.vel.x = 0 # increment in x direction self.rect.left += self.vel.x # do x-axis collisions self.collide(self.vel.x, 0, self.platforms) # increment in y direction self.rect.top += self.vel.y # assuming we're in the air self.onGround = False; # do y-axis collisions self.collide(0, self.vel.y, self.platforms) def collide(self, xvel, yvel, platforms): for p in platforms: if pygame.sprite.collide_rect(self, p): if isinstance(p, ExitBlock): pygame.event.post(pygame.event.Event(QUIT)) if xvel > 0: self.rect.right = p.rect.left if xvel < 0: self.rect.left = p.rect.right if yvel > 0: self.rect.bottom = p.rect.top self.onGround = True self.vel.y = 0 if yvel < 0: self.rect.top = p.rect.bottom class Platform(Entity): def __init__(self, pos, *groups): super().__init__(Color("#DDDDDD"), pos, *groups) class ExitBlock(Entity): def __init__(self, pos, *groups): super().__init__(Color("#0033FF"), pos, *groups) if __name__ == "__main__": main()
Since right know, you have a static background, and the player that you control, is blitted in the position he is in, you have 2 options to always show the character in the middle.
If you map is small enought, you can have a big img A, and derive a rectangle, based on the position of the player that will be the size of the screen. That way, the player will always be in the middle. A Rect.clamp(Rect) or Rect.clamp_ip(Rect) will aid you in that.
Another approach is to have a different tuple for position on screen. The player will have a constant value in the center of the screen, while the backgrounds position will be the negative of the player position.
The only way to do that is to separate logical positions in the map, from physical positions on the screen .
Any code related to actually drawing your map on the screen - in your case all the
.rect attributes of your sprites - have to do so based on an offset of what part of yor map the screen is actually using.
For example, your screen might be showing your map starting with position (10,10) on the top left - all display related code them (which in the case above are the
.rectattributes) should subtract the screen offset from the current logical position - (say the character is at map coords(12,15) - so, it should be drawn at (12,15) - (10, 10) -> (2, 5) * BLOCK_SIZE)
In your example above BLOCK_SIZE is hardcoded to 32,32, so you want to draw it at physical pixel position (2 * 32, 5 * 32) on the display)
(hint: avoid hardcoding things this way, make it a constant declaration at the beginning of your code)
Unfortunately, Pygame doesn't have a built-in solution to this problem. Pygame use
pygame.sprite.Sprite objects organized in
pygame.sprite.Groups. The attribute
.rect of the Sprites is used for drawing the objects as well as for the collision test between objects. There is no built-in feature that can convert object coordinates to screen coordinates before drawing.
As a suggestion for the developers of Pygame: It would be nice to have an optional argument for the camera offset in the method
There a different approaches:
Instead of moving the player, you can move any object in the scene in the opposite direction. This is the worst of all approaches. I strongly recommend not doing this. Every time you add a new object you need to make sure that it moves as the player moves. Dealing with object animation or floating point accuracy can turn out to be a nightmare.
Create a virtual screen size of the world and draw the entire screen on the virtual screen. At the end of each frame, a subsection of the map is displayed on the screen.
virtual_screen = pygame.Surface((map_width, map_height))
There are 2 possibilities. You can
blitan area of the virtual screen directly on the screen by specifying the area argument:
camera_area = pygame.Rect(camera_x, camera_y, camera_width, camera_height) screen.blit(virtual_screen, (0, 0), camera_area)
The other possibility is to define a subsurface that is linked directly to the source surface using the
camera_area = pygame.Rect(camera_x, camera_y, camera_width, camera_height) camera_subsurf = source_surf.subsurface(camera_area)
screen.blit(camera_subsurf, (0, 0))
The disadvantage of this approach is that it can have a very large memory footprint. If the virtual screen is huge, the game will lag. This solution is only suitable if the size of the game area is not much larger than the screen. As a rule of thumb, if the play area is more than twice the size of the screen, you shouldn't go this way (I'm talking about twice the size of the area, not twice the length of its width and height).
For large play areas, the only approach that can be used is to add an offset to the objects before drawing:
offset_x = -camera_x offset_y = -camera_y for object in objects: screen.blit(object.image, (object.rect.x + offset_x, object.rect.y + offset_y))
pygame.sprite.Group.drawcan not be used directly in this case. This approach is detailed in a highly rated answer.
Alternatively, you can move all of the sprites before drawing them:
all_sprites = pygame.sprite.Group()
for sprite in all_sprites: all_sprites.rect.move_ip(-camera_x, -camera_y) all_sprites.draw(screen) for sprite in all_sprites: all_sprites.rect.move_ip(camera_x, camera_y)
At the end a comment about dirty mechanisms and partial screen updates: As soon as the player moves, the entire screen is dirty and needs to be updated. It is therefore questionable whether you should invest resources in partial update mechanisms. These algorithms also take time to run. In highly dynamic scenes, the result of the algorithm is to update all.