Search code examples
pythonpygamecamera

Basic camera system for my pygame platformer


Hi I am currently trying to create a platformer and I tried creating a camera system for it. But after many tries I still cannot find a good way to do it even after having tried everything I saw online. I would really like the camera to work a bit like this animation I found here Any advices on how I should approach this?

import pygame,sys

pygame.init()

# Settings
tile_size = 64
fov = 10
screen_width = 1200
screen_height = tile_size * fov

level_data = [
'                       ',
'                       ',
'                  XX   ',
'XX    XXX              ',
'XX                   XX',
'XXXX        XX         ',
'XXXX  P   XX           ',
'XX    X  XXX    XX  X  ',
'      X  XXX    XX  XX ',
'   XXXX  XXXXX  XX  XXX',
'XXXXXXX  XXXXX  XX  XXX']

# Classes
class Game():
    def __init__(self):
        self.level = Level(screen)
        self.status = 'level'

    def run(self):
        if self.status == 'level':
            self.level.run()

class Level:
    def __init__(self,display_surface):
        # Basic setup
        self.setup_level()
        self.display_surface = display_surface

        # Movement
        self.x_shift = 0
        self.y_shift = 0

    def setup_level(self):
        self.tiles = pygame.sprite.Group()
        self.player = pygame.sprite.GroupSingle()
        for row_index,row in enumerate(level_data):
            for col_index,col in enumerate(row):
                x = col_index * tile_size
                y = (row_index * tile_size) - ((len(level_data) - fov) * tile_size)
                if col == 'X':
                    tile = Tile((x,y))
                    self.tiles.add(tile)
                if col == 'P':
                    player = Player((x,y))
                    self.player.add(player)

    def vertical_movement_collision(self):
        player = self.player.sprite
        player.apply_gravity()

        for sprite in self.tiles.sprites():
            if sprite.rect.colliderect(player.rect):
                if player.direction.y > 0:
                    player.rect.bottom = sprite.rect.top
                    player.on_ground = True
                elif player.direction.y < 0:
                    player.rect.top = sprite.rect.bottom
                player.direction.y = 0

        if player.on_ground and player.direction.y > 1 or player.direction.y < 0:
            player.on_ground = False

    def horizontal_movement_collision(self):
        player = self.player.sprite
        player.rect.x += player.direction.x * player.speed

        for sprite in self.tiles.sprites():
            if sprite.rect.colliderect(player.rect):
                if player.direction.x > 0:
                    player.rect.right = sprite.rect.left
                elif player.direction.x < 0:
                    player.rect.left = sprite.rect.right

    def run(self):
        self.vertical_movement_collision()
        self.horizontal_movement_collision()

        # Tiles
        self.tiles.update(self.x_shift,self.y_shift)
        self.tiles.draw(self.display_surface)

        # Player
        self.player.update()
        self.player.draw(self.display_surface)

class Tile(pygame.sprite.Sprite):
    def __init__(self,pos):
        super().__init__()
        self.image = pygame.Surface((tile_size,tile_size))
        self.image.fill('grey')
        self.rect = self.image.get_rect(topleft = pos)
        self.x = self.rect.x
        self.y = self.rect.y

    def update(self,x_shift,y_shift):
        self.x += x_shift
        self.y -= y_shift
        self.rect.topleft = round(self.x), round(self.y)

class Player(pygame.sprite.Sprite):
    def __init__(self,pos):
        super().__init__()
        # Image
        self.image = pygame.Surface((32,64))
        self.image.fill('red')
        self.rect = self.image.get_rect(topleft = pos)

        # Movement
        self.direction = pygame.math.Vector2(0,0)
        self.speed = 8
        self.gravity = 0.8
        self.jump_speed = -20

        # Status
        self.on_ground = False

    def get_inputs(self):
        keys = pygame.key.get_pressed()

        if keys[pygame.K_SPACE]:
            self.jump()
        if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
            self.direction.x = 1
        elif keys[pygame.K_a] or keys[pygame.K_LEFT]:
            self.direction.x = -1
        else:
            self.direction.x = 0

    def apply_gravity(self):
        self.direction.y += self.gravity
        self.rect.y += self.direction.y

    def jump(self):
        if self.on_ground:
            self.direction.y = self.jump_speed

    def update(self):
        self.get_inputs()

# Game setup
screen = pygame.display.set_mode((screen_width,screen_height))
pygame.display.set_caption('Platformer')
clock = pygame.time.Clock()
game = Game()

# Main
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    
    screen.fill('black')
    game.run()

    pygame.display.update()
    clock.tick(60)

Solution

  • In basic camera system you keep player in center. So camera depends on player position and screen size

    camera_x = -self.player.sprite.rect.centerx + self.display_surface.get_rect().centerx
    camera_y = -self.player.sprite.rect.centery + self.display_surface.get_rect().centery
    
    camera = (camera_x, camera_y) 
    

    and later you have to add camera to all sprites when you blit it - but not add this to sprite position.

        def draw(self, screen, camera):
            screen.blit(self.image, self.rect.move(camera))
    

    But it needs to run draw() for every sprite separatelly because group.draw() runs directly blit(self.image, self.rect)


    For more complex camera system you have to write more complex code which checks player position and change it in different way (using if/else)


    Minimal working code:

    import pygame,sys
    
    # Classes
    class Game():
        def __init__(self):
            self.level = Level(screen)
            self.status = 'level'
    
        def run(self):
            if self.status == 'level':
                self.level.run()
    
    class Level:
        def __init__(self,display_surface):
            # Basic setup
            self.setup_level()
            self.display_surface = display_surface
    
            # Movement
            self.x_shift = 0
            self.y_shift = 0
    
        def setup_level(self):
            self.tiles = pygame.sprite.Group()
            self.player = pygame.sprite.GroupSingle()
            for row_index,row in enumerate(level_data):
                for col_index,col in enumerate(row):
                    x = col_index * tile_size
                    y = (row_index * tile_size) - ((len(level_data) - fov) * tile_size)
                    if col == 'X':
                        tile = Tile((x,y))
                        self.tiles.add(tile)
                    if col == 'P':
                        player = Player((x,y))
                        self.player.add(player)
    
        def vertical_movement_collision(self):
            player = self.player.sprite
            player.apply_gravity()
    
            for sprite in self.tiles.sprites():
                if sprite.rect.colliderect(player.rect):
                    if player.direction.y > 0:
                        player.rect.bottom = sprite.rect.top
                        player.on_ground = True
                    elif player.direction.y < 0:
                        player.rect.top = sprite.rect.bottom
                    player.direction.y = 0
    
            if player.on_ground and player.direction.y > 1 or player.direction.y < 0:
                player.on_ground = False
    
        def horizontal_movement_collision(self):
            player = self.player.sprite
            player.rect.x += player.direction.x * player.speed
    
            for sprite in self.tiles.sprites():
                if sprite.rect.colliderect(player.rect):
                    if player.direction.x > 0:
                        player.rect.right = sprite.rect.left
                    elif player.direction.x < 0:
                        player.rect.left = sprite.rect.right
    
        def run(self):
            self.vertical_movement_collision()
            self.horizontal_movement_collision()
    
            # Tiles
            self.tiles.update(self.x_shift,self.y_shift)
            self.player.update()
            
            camera_x = -self.player.sprite.rect.centerx + self.display_surface.get_rect().centerx
            camera_y = -self.player.sprite.rect.centery + self.display_surface.get_rect().centery
            camera = (camera_x, camera_y) 
            print(camera)
            
            for item in self.tiles:
                item.draw(self.display_surface, camera)
    
            self.player.sprite.draw(self.display_surface, camera)
    
    class Tile(pygame.sprite.Sprite):
        def __init__(self,pos):
            super().__init__()
            self.image = pygame.Surface((tile_size,tile_size))
            self.image.fill('grey')
            self.rect = self.image.get_rect(topleft = pos)
            self.x = self.rect.x
            self.y = self.rect.y
    
        def update(self,x_shift,y_shift):
            self.x += x_shift
            self.y -= y_shift
            self.rect.topleft = round(self.x), round(self.y)
    
        def draw(self, screen, camera):
            screen.blit(self.image, self.rect.move(camera))
        
    class Player(pygame.sprite.Sprite):
        def __init__(self,pos):
            super().__init__()
            # Image
            self.image = pygame.Surface((32,64))
            self.image.fill('red')
            self.rect = self.image.get_rect(topleft = pos)
    
            # Movement
            self.direction = pygame.math.Vector2(0,0)
            self.speed = 8
            self.gravity = 0.8
            self.jump_speed = -20
    
            # Status
            self.on_ground = False
    
        def get_inputs(self):
            keys = pygame.key.get_pressed()
    
            if keys[pygame.K_SPACE]:
                self.jump()
            if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
                self.direction.x = 1
            elif keys[pygame.K_a] or keys[pygame.K_LEFT]:
                self.direction.x = -1
            else:
                self.direction.x = 0
    
        def apply_gravity(self):
            self.direction.y += self.gravity
            self.rect.y += self.direction.y
    
        def jump(self):
            if self.on_ground:
                self.direction.y = self.jump_speed
    
        def update(self):
            self.get_inputs()
    
        def draw(self, screen, camera):
            screen.blit(self.image, self.rect.move(camera))
    
    # --- main ---
    
    pygame.init()
    
    # Settings
    tile_size = 64
    fov = 10
    screen_width = 1200
    screen_height = tile_size * fov
    
    level_data = [
    '                       ',
    '                       ',
    '                  XX   ',
    'XX    XXX              ',
    'XX                   XX',
    'XXXX        XX         ',
    'XXXX  P   XX           ',
    'XX    X  XXX    XX  X  ',
    '      X  XXX    XX  XX ',
    '   XXXX  XXXXX  XX  XXX',
    'XXXXXXX  XXXXX  XX  XXX']
    
    
    
    # Game setup
    screen = pygame.display.set_mode((screen_width,screen_height))
    pygame.display.set_caption('Platformer')
    clock = pygame.time.Clock()
    game = Game()
    
    # Main
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        
        screen.fill('black')
        game.run()
    
        pygame.display.update()
        clock.tick(60)
    

    EDIT:

    More complex camera system - it doesn't move map when player is near the border of map.

        def run(self):
            self.vertical_movement_collision()
            self.horizontal_movement_collision()
    
            # Tiles
            self.tiles.update(self.x_shift, self.y_shift)
            self.player.update()
            
            map_left   = 0
            map_right  = len(level_data[0])*tile_size
            map_top    = 0
            map_bottom = len(level_data)*tile_size
            
            camera_x = -self.player.sprite.rect.centerx + self.display_surface.get_rect().centerx
            
            if self.player.sprite.rect.centerx < map_left + self.display_surface.get_rect().centerx:
                camera_x = -map_left
            if self.player.sprite.rect.centerx > map_right - self.display_surface.get_rect().centerx:
                camera_x = -(map_right - self.display_surface.get_rect().width)
    
            camera_y = -self.player.sprite.rect.centery + self.display_surface.get_rect().centery
    
            if self.player.sprite.rect.centery < map_top + self.display_surface.get_rect().centery:
                camera_y = -map_top
            if self.player.sprite.rect.centery > map_bottom - self.display_surface.get_rect().centery:
                camera_y = -(map_bottom - self.display_surface.get_rect().height)
            
            camera = (camera_x, camera_y) 
            
            for item in self.tiles:
                item.draw(self.display_surface, camera)
    
            self.player.sprite.draw(self.display_surface, camera)
    

    but in setup_level() it needs y without - ((len(level_data) - fov) * tile_size)

    y = (row_index * tile_size) # - ((len(level_data) - fov) * tile_size)
    

    Full code:

    import pygame,sys
    
    # Classes
    class Game():
        def __init__(self):
            self.level = Level(screen)
            self.status = 'level'
    
        def run(self):
            if self.status == 'level':
                self.level.run()
    
    class Level:
        def __init__(self,display_surface):
            # Basic setup
            self.setup_level()
            self.display_surface = display_surface
    
            # Movement
            self.x_shift = 0
            self.y_shift = 0
    
        def setup_level(self):
            self.tiles = pygame.sprite.Group()
            self.player = pygame.sprite.GroupSingle()
            for row_index,row in enumerate(level_data):
                for col_index,col in enumerate(row):
                    x = col_index * tile_size
                    y = (row_index * tile_size)# - ((len(level_data) - fov) * tile_size)
                    if col == 'X':
                        tile = Tile((x,y))
                        self.tiles.add(tile)
                    if col == 'P':
                        player = Player((x,y))
                        self.player.add(player)
    
        def vertical_movement_collision(self):
            player = self.player.sprite
            player.apply_gravity()
    
            for sprite in self.tiles.sprites():
                if sprite.rect.colliderect(player.rect):
                    if player.direction.y > 0:
                        player.rect.bottom = sprite.rect.top
                        player.on_ground = True
                    elif player.direction.y < 0:
                        player.rect.top = sprite.rect.bottom
                    player.direction.y = 0
    
            if player.on_ground and player.direction.y > 1 or player.direction.y < 0:
                player.on_ground = False
    
        def horizontal_movement_collision(self):
            player = self.player.sprite
            player.rect.x += player.direction.x * player.speed
    
            for sprite in self.tiles.sprites():
                if sprite.rect.colliderect(player.rect):
                    if player.direction.x > 0:
                        player.rect.right = sprite.rect.left
                    elif player.direction.x < 0:
                        player.rect.left = sprite.rect.right
    
        def run(self):
            self.vertical_movement_collision()
            self.horizontal_movement_collision()
    
            # Tiles
            self.tiles.update(self.x_shift, self.y_shift)
            self.player.update()
            
            map_left   = 0
            map_right  = len(level_data[0])*tile_size
            map_top    = 0
            map_bottom = len(level_data)*tile_size
            
            camera_x = -self.player.sprite.rect.centerx + self.display_surface.get_rect().centerx
            
            if self.player.sprite.rect.centerx < map_left + self.display_surface.get_rect().centerx:
                camera_x = -map_left
            if self.player.sprite.rect.centerx > map_right - self.display_surface.get_rect().centerx:
                camera_x = -(map_right - self.display_surface.get_rect().width)
    
            camera_y = -self.player.sprite.rect.centery + self.display_surface.get_rect().centery
    
            if self.player.sprite.rect.centery < map_top + self.display_surface.get_rect().centery:
                camera_y = -map_top
            if self.player.sprite.rect.centery > map_bottom - self.display_surface.get_rect().centery:
                camera_y = -(map_bottom - self.display_surface.get_rect().height)
            
            camera = (camera_x, camera_y) 
            
            for item in self.tiles:
                item.draw(self.display_surface, camera)
    
            self.player.sprite.draw(self.display_surface, camera)
    
    class Tile(pygame.sprite.Sprite):
        def __init__(self,pos):
            super().__init__()
            self.image = pygame.Surface((tile_size,tile_size))
            self.image.fill('grey')
            self.rect = self.image.get_rect(topleft = pos)
            self.x = self.rect.x
            self.y = self.rect.y
    
        def update(self, x_shift, y_shift):
            self.x += x_shift
            self.y -= y_shift
            self.rect.topleft = round(self.x), round(self.y)
    
        def draw(self, screen, camera):
            screen.blit(self.image, self.rect.move(camera))
        
    class Player(pygame.sprite.Sprite):
        def __init__(self,pos):
            super().__init__()
            # Image
            self.image = pygame.Surface((32,64))
            self.image.fill('red')
            self.rect = self.image.get_rect(topleft = pos)
    
            # Movement
            self.direction = pygame.math.Vector2(0,0)
            self.speed = 8
            self.gravity = 0.8
            self.jump_speed = -20
    
            # Status
            self.on_ground = False
    
        def get_inputs(self):
            keys = pygame.key.get_pressed()
    
            if keys[pygame.K_SPACE]:
                self.jump()
            if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
                self.direction.x = 1
            elif keys[pygame.K_a] or keys[pygame.K_LEFT]:
                self.direction.x = -1
            else:
                self.direction.x = 0
    
        def apply_gravity(self):
            self.direction.y += self.gravity
            self.rect.y += self.direction.y
    
        def jump(self):
            if self.on_ground:
                self.direction.y = self.jump_speed
    
        def update(self):
            self.get_inputs()
    
        def draw(self, screen, camera):
            screen.blit(self.image, self.rect.move(camera))
    
    # --- main ---
    
    pygame.init()
    
    # Settings
    tile_size = 64
    fov = 10
    screen_width = 1200
    screen_height = tile_size * fov
    
    level_data = [
    '                       ',
    '                       ',
    '                  XX   ',
    'XX    XXX              ',
    'XX                   XX',
    'XXXX        XX         ',
    'XXXX  P   XX           ',
    'XX    X  XXX    XX  X  ',
    '      X  XXX    XX  XX ',
    '   XXXX  XXXXX  XX  XXX',
    'XXXXXXX  XXXXX  XX  XXX']
    
    
    # Game setup
    screen = pygame.display.set_mode((screen_width,screen_height))
    pygame.display.set_caption('Platformer')
    clock = pygame.time.Clock()
    game = Game()
    
    # Main
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        
        screen.fill('black')
        game.run()
    
        pygame.display.update()
        clock.tick(60)