Add Scrolling to a Platformer in Pygame

Add scrolling to a platformer in pygame

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.

Using 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[0])*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[0] + WIN_WIDTH/2
y = -target_rect.center[1] + 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 min/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:

Sample Image

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 pygame.key.get_pressed instead


 #! /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[0] + SCREEN_SIZE.width/2
y = -self.target.rect.center[1] + 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[0])*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()

Positioning and adding enemies in a pygame 2d scrolling platformer?

for the collision with the player i recommend you something like this:

#in your gameloop
playerEnemyCollision = pygame.sprite.spritecollide(player, enemies, False)

"enemies" needs to be a sprite-group.To create a sprite group:

#outside your gameloop
enemies = pygame.sprite.Group()

To create a new Enemy and add it to the group, just typ:

#outside your gameloop
en = Enemy()
en.rect.x = XX #set your Enemies x-Position
en.rect.y = YY #set your Enemies y-Position
en.add(enemies) #adds the enemy "en" to the sprite-group "enemies"

Now you can check for a collision with:

#in your gameloop
if playerEnemyCollision:
#your "kill-player-code" goes her
#Example:
player.kill()

It is at the most time not such a good idea to change a sprite's location for normal movement outside your "Enemy-Class".
I hope i could help you with your question.
Twistios

Pygame Platformer Scrolling Bug

The problem is in your scroll, or rather your shift_world function. You shift every object in self.object_list. For Level01, this list only contains the block objects. In scroll, you then shift every block and the player, but not the enemy. This means the enemy sprite stays at its place and is not shifted with the world as it should be. It therefore appears to move, because its position relative to the world changes. In truth, it stays blitted at the same position on the canvas as it was.

When the player jumps, the enemy ends up in the air once the world has shifted down, and then gravity pulls him back to the platform. When the player moves right, the enemy seems to follow because the world shifts left and the enemy doesn't shift with it.

Add the enemy to your object_list and it should work as expected.

Pygame Scrolling

So here we go. Here you are updating the platforms if the robot reached the limit:

for plat in platforms:
plat.rect.x = plat.rect.x - abs(Robot.vel.x)

But when you draw the platforms you draw from the original PLATFORM_LIST list:

def draw():
for plat in PLATFORM_LIST:
p = Platform(*plat)
pg.draw.rect(gameDisplay, blue, (p))

So what ends up happening is even though you are updating the platforms properly, you are drawing from the original list, so you are not drawing the updated the list. You should draw from the platform list you are updating:

def draw():
for plat in platforms:
pg.draw.rect(gameDisplay, blue, plat)

Second, I discovered once you hit the left scroll limit, the movement back to the right direction moved the robot the wrong way. replace this:

if Robot.rect.left < display_width/4:
Robot.RobotPos.x = Robot.RobotPos.x - abs(Robot.vel.x)

with this (the minus sign switched over to the plus sign):

if Robot.rect.left < display_width/4:
Robot.RobotPos.x = Robot.RobotPos.x + abs(Robot.vel.x)

Just a couple things I found while playing around with the game.

Update

There is also an issue with rounding. Pygame rectangles take integers. Your calculation of velocity yields a float and you are trying to add that to the x of the rectangle here:

for plat in platforms:
plat.rect.x = plat.rect.x - abs(Robot.vel.x)

This causes issues with rounding that show up in the display (platforms) moving in peculiar ways. You can make them ints:

for plat in platforms:
plat.rect.x = plat.rect.x - int(abs(Robot.vel.x))

Otherwise you will have to make the platforms like you do the Robot and deal in vec()

Trying to make screen center to player in Pygame simple 2D Platformer

You should move the camera like this:

# at the beginning: set camera
camera = pygame.math.Vector2((0, 0))

# in the main loop: adjust the camera position to center the player
camera.x = player.x_pos - resx / 2
camera.y = player.y_pos - resy / 2

# in each sprite class: move according to the camera instead of changing the sprite position
pos_on_the_screen = (self.x_pos - camera.x, self.y_pos - camera.y)
  • The camera follows the player, and places itself so that the player is in the center of the screen
  • The position of each sprite never changes, this is a very bad idea to constantly change the position of the sprites.
  • Each sprite is drawn on the screen depending of the camera

To reduce lag, you should display each sprite only if it is visible:

screen_rect = pygame.Rect((0, 0), (resx, resy))
sprite_rect = pygame.Rect((self.x_pos - camera.x, self.y_pos - camera.y), sprite_image.get_size())
if screen_rect.colliderect(sprite_rect):
# render if visible

Here is a screenshot of a moving background in a game I made, using the same method:

Sample Image

Here I move the camera more smoothly. Basically I use this method:

speed = 1
distance_x = player.x_pos - camera.x
distance_y = player.y_pos - camera.y
camera.x = (camera.x * speed + distance_x) / (speed + 1)
camera.y = (camera.y * speed + distance_y) / (speed + 1)

And I change speed according to the FPS.



Related Topics



Leave a reply



Submit