Evelyn
Evelyn

Reputation: 192

How to make parallax scrolling work properly with a camera that stops at edges pygame

Say we have a game made in pygame, and in this game, we have "scroll" objects which basically boxes the camera scrolls within, stopping at the edges. When the player leaves a scroll and enters a new one, the camera moves from one scroll to the other. In this situation, to achieve a parallax effect, we would need to divide the amount the camera scrolls by a value. However, if we just do this alone, when we move into a new "scroll", the background will appear to be shifted by an incorrect amount relative to how much it scrolled in the first "scroll", how would one go about fixing this? Here is my camera code, and the scroll code,

import pygame
from pygame.locals import *

class scroll_handler(list):
    def __init__(self):
        self.primary_scroll = None
        self.append(scroll_obj((50+1)*16,(2)*16,65*16,30*16))
        self.append(scroll_obj((51+65)*16,(2)*16,65*16,30*16))

    def update(self,player):
        for i in self:
            if i.entity_in_scroll(player):
                self.primary_scroll = i

class scroll_obj(pygame.Rect):
    def __init__(self,x,y,sx,sy,enable_backgrounds = True):
        super().__init__((x,y),(sx,sy))
        self.enable_backgrounds = enable_backgrounds

    def entity_in_scroll(self,entity):
        return self.contains(entity)

class camera(object):
    def __init__(self, camera_func, widthy, heighty):
        self.state = pygame.Rect((16,16),(widthy,heighty))
        self.camera_func = camera_func
        self.scroll = scroll_obj(16,16,widthy,heighty)

    def update_scroll(self, scroll):
        self.state = pygame.Rect(scroll.x, scroll.y, scroll.width, scroll.height)

    def apply(self, target, state = 0, parallax = False):
        if not parallax:
            return target.move(self.state.topleft)
        if parallax:
            return target.move(state.topleft)

    def update(self, target, parallax_x = 1, parallax_y = 1):
        self.update_scroll(self.scroll)
        self.state = self.camera_func( self.state, target, parallax_x, parallax_y)
    
def complex_camera(camera, target_rect, parallax_x=1, parallax_y=1):
    l, t, _, _ = target_rect
    _, _, w, h = camera
    l, t, _, _ = -l+(320), -t+(180), w, h
    
    l = min(-camera.x, l)                                       # stop scrolling at the left edge
    l = max(-(camera.x + camera.width-640), l)                  # stop scrolling at the right edge
    l = round(l/(parallax_x),2)                         # apply parallax in x direction
        
    t = max(-(camera.y + camera.height-360), t)                # stop scrolling at the bottom   
    t = min(-camera.y, t)                                       # stop scrolling at the top
    t = round(t/(parallax_y),2)                         # apply parallax in y direction
    return pygame.Rect(l, t, w, h)

here is my code for backgrounds and applying the parallax effect to them

import pygame
class background(pygame.Rect):
    def __init__(self,x,y,image):
        super().__init__(x*16,y*16,1024,512)
        self.image = image
        self.CAX = self.x
        self.CAY = self.y
        self.rect = pygame.Rect(self.CAX,self.CAY,1024,512) # an additional rect object used elsewhere in another code file, ignore this
        
    def apply_camera(self, state, GAME):
        cam = GAME.camera.apply(self, state, True)
        self.CAX = cam[0]
        self.CAY = cam[1]
        self.rect.x = self.CAX
        self.rect.y = self.CAY
        
        
class backgrounds(dict):
    def __init__(self,GAME):
        self.GAME = GAME
        

        BH = self.GAME.background_handler
        self["background_0"] = [background(0,0,BH[0])]
        self["background_1"] = [background(0,1,BH[3])]
        self["background_2"] = [background(0,2,BH[2])]
        self["background_3"] = [background(0,3,BH[1])]

    def get_background(self):
        self.background_0 = []
        self.background_1 = []
        self.background_2 = []
        self.background_3 = []
        
        original = self.GAME.camera.state
        half = pygame.Rect(int(original[0]/2), int(original[1]/2), self.GAME.camera.state.width, self.GAME.camera.state.height)
        quater = pygame.Rect(int(original[0]/4), int(original[1]/4), self.GAME.camera.state.width, self.GAME.camera.state.height)
        eighth = pygame.Rect(int(original[0]/8), int(original[1]/8), self.GAME.camera.state.width, self.GAME.camera.state.height)
        fortieth = pygame.Rect(int(original[0]/40), int(original[1]/40), self.GAME.camera.state.width, self.GAME.camera.state.height)

        for i in self["background_0"]:
            
                i.apply_camera(fortieth,self.GAME)
                self.background_0.append([(i.CAX,i.CAY),(i.image)])
        
        for i in self["background_1"]:
            
                i.apply_camera(eighth,self.GAME)
                self.background_1.append([(i.CAX,i.CAY),(i.image)])
    
        for i in self["background_2"]:

                i.apply_camera(quater,self.GAME)
                self.background_2.append([(i.CAX,i.CAY),(i.image)])

        for i in self["background_3"]:
                i.apply_camera(half,self.GAME)
                self.background_3.append([(i.CAX,i.CAY),(i.image)])
                
        return (self.background_0), (self.background_1), (self.background_2), (self.background_3)

what do I need to change in order to make it such that when entering a new scroll, the background will be in the appropriate position and not moved significantly to the right?

NOTE: The camera and scroll code are in separate modules, don't worry about this if it seems odd

Upvotes: 4

Views: 1277

Answers (1)

Rabbid76
Rabbid76

Reputation: 211166

Scrolling the background is achieved by moving the background image and drawing the background image multiple times with an offset on the display:

def draw_background(surf, bg_image, bg_x, bg_y):
    surf.blit(bg_image, (bg_x - surf.get_width(), bg_y))
    surf.blit(bg_image, (bg_x, bg_y))

For a background parallax effect, you need several background layers that you must move at different speeds. The back layers must be moved more slowly than the front layers. e.g.:

bg_layer_1_x = (bg_layer_1_x - move_x) % 600
bg_layer_2_x = (bg_layer_2_x - move_x*0.75) % 600
bg_layer_3_x = (bg_layer_3_x - move_x*0.5) % 600

Minimal example

import pygame

pygame.init()
window = pygame.display.set_mode((600, 400))
clock = pygame.time.Clock()

tree1_image = pygame.Surface((100, 100), pygame.SRCALPHA)
pygame.draw.rect(tree1_image, (64, 32, 0), (45, 75, 10, 25))
pygame.draw.polygon(tree1_image, (16, 64, 0), [(50, 0), (70, 35), (65, 35), (90, 75), (10, 75), (35, 35), (30, 35)])
tree2_image = pygame.Surface((100, 100), pygame.SRCALPHA)
pygame.draw.rect(tree2_image, (128, 64, 16), (45, 75, 10, 25))
pygame.draw.polygon(tree2_image, (64, 128, 0), [(50, 0), (70, 35), (65, 35), (90, 75), (10, 75), (35, 35), (30, 35)])
tree2_image = pygame.transform.smoothscale(tree2_image, (75, 75))
tree3_image = pygame.Surface((100, 100), pygame.SRCALPHA)
pygame.draw.rect(tree3_image, (164, 128, 96), (45, 75, 10, 25))
pygame.draw.polygon(tree3_image, (128, 164, 64), [(50, 0), (70, 35), (65, 35), (90, 75), (10, 75), (35, 35), (30, 35)])
tree3_image = pygame.transform.smoothscale(tree3_image, (50, 50))

bg_layer_1_image = pygame.Surface((600, 100), pygame.SRCALPHA)
for i in range(6):
    bg_layer_1_image.blit(tree1_image, (i*100, 0))
bg_layer_2_image = pygame.Surface((600, 75), pygame.SRCALPHA)
for i in range(8):
    bg_layer_2_image.blit(tree2_image, (i*75, 0))  
bg_layer_3_image = pygame.Surface((600, 50), pygame.SRCALPHA)
for i in range(12):
    bg_layer_3_image.blit(tree3_image, (i*50, 0))    
bg_layer_1_x = 0
bg_layer_2_x = 0
bg_layer_3_x = 0
move_x = 1

def draw_background(surf, bg_image, bg_x, bg_y):
    surf.blit(bg_image, (bg_x - surf.get_width(), bg_y))
    surf.blit(bg_image, (bg_x, bg_y))

run = True
while run:
    clock.tick(100)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False 

    bg_layer_1_x = (bg_layer_1_x - move_x) % 600
    bg_layer_2_x = (bg_layer_2_x - move_x*0.75) % 600
    bg_layer_3_x = (bg_layer_3_x - move_x*0.5) % 600

    window.fill((192, 192, 255))
    pygame.draw.rect(window, (64, 128, 64), (0, 250, 600, 150))
    pygame.draw.rect(window, (128, 164, 192), (0, 170, 600, 80))
    draw_background(window, bg_layer_3_image, bg_layer_3_x, 120)
    draw_background(window, bg_layer_2_image, bg_layer_2_x, 135)
    draw_background(window, bg_layer_1_image, bg_layer_1_x, 150)
    pygame.display.flip()

pygame.quit()
exit()

Also see PyGameExamplesAndAnswers - Background

Upvotes: 0

Related Questions