0
\$\begingroup\$

I know many questions have already been asked on this same topic, but I can't figure out from those how to solve my problem (possibly due to a different approach to achieve the same goal: scrolling my camera keeping the main character of my game centered in the screen).

I am developing my first 2D platform game with python and pygame.

I am having difficulties in making a camera which follows the player each times he moves, keeping him in the center of the screen.

What is happening is that the camera keeps following the main character as he passes over the center of the screen, but, as he keeps moving in one direction (say leftward), he suddenly keeps closer to the border towards he is moving (i.e. the camera "is too slow" in following him...). I know it has probably to do with the camera shift not taking the hero horizontal and vertical speed, but I don't know how to solve this problem at the moment.

I have two separate classes for the main character and the camera. I am about to attach a bit of my code (it seems a lot but it's "only" the relevant part of code to understand the problem, and there's plenty of spaces and comments...). I hope someone could give a good hint!

The Hero class is actually a child of Character class, so here are both:

class Character(pygame.sprite.Sprite):
    """class with generic character's attrs and methods
    (intended to be inherited from players and enemies in the game)"""

    def __init__(self, x, y, life, img):
        super().__init__() # Call the parent class (Sprite) constructor

        self.img = img

        self.image = pygame.image.load(os.path.join(SPRITE_FOLDER, img)).convert_alpha()
        self.rect = self.image.get_rect()

        self.x = x
        self.y = y
        self.rect.x, self.rect.y = self.x, self.y

        self.hspeed = 5
        self.vspeed = 0

        self.life = life

    def gravity_force(self, gravity = .35):
        if self.vspeed == 0:
            self.vspeed = 1
        else:
            self.vspeed += gravity

    def collision(self, block_list = pygame.sprite.Group()):
        """hanlde collisions between hero and environment.
        'collision_list' should contain a list of sprite against
        which collision is detected for the main character"""

        self.gravity_force()
        self.rect.top += self.vspeed # let the hero falling

        # See if the character Sprite has collided with anything in the Group block_list
        # (returns a Sprite_list)
        collision_list = pygame.sprite.spritecollide(self, block_list, False)
        ()
        # check for collisions in each direction
        for colliding in collision_list:
            if self.vspeed > 0: # if is falling...
                self.rect.bottom = colliding.rect.top # ... if hits a block, place it on it...
                self.vspeed = 0 # ... and stop vertical speed

class Hero(Character):
    """main Hero class"""

    # If you want to extend the parent's __init__() method, you should do:
    # def __init__(self, x, y, life, img, *args):
    #     super().__init__(x, y, life, img)
    #     REDEFINE super() methods and add additional attributes

    def movement(self, event = None):
        """handle character's interaction with the level and keyboard inputs"""

        if event:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    self.rect.left += self.hspeed # move topleft rectangle 'hspeed' pixels left
                if event.key == pygame.K_LEFT:
                    self.rect.left -= self.hspeed # move topleft rectangle 'hspeed' pixels right
                # if event.key == pygame.K_UP:
                #     self.rect.top += 5

And here is the Camera class (a rectangle with size equal to the screen, whose center coordinates are moved to match the Hero's ones each frame):

class Camera():
    """Surface containing the Hero at its center and all objects nearby it"""

    def __init__(self, screen):
        """create a camera Rect whose size is equal to screen size"""
        self.rect = screen.get_rect()
        self.x = self.rect.x
        self.y = self.rect.y
        self.dx = 0 # initialize camera x shift
        self.dy = 0 # initialize camera y shift

    def follow_target(self, target):
        """move the camera so that target Rect is always centered"""
        self.rect.clamp_ip(target.rect) # center camera to target

        # do not accept negative coords
        if self.rect.x < 0: self.rect.x = 0
        if self.rect.y < 0: self.rect.y = 0

        # calculate dx, dy based on previous and current camera position
        self.dx = self.x - self.rect.x
        self.dy = self.y - self.rect.y

        # set current camera pos permanently
        self.x = self.rect.x
        self.y = self.rect.y

    def apply_shift(self, target):
        """move target Rect according to camera pos"""
        target.rect.move_ip(self.dx, self.dy) # move target position of a given offset

And finally the parts in the parts of my main() function of the game relevant to my issue:

screen.blit(level.background, (0, 0)) # draw background
camera = Camera(screen) # creates camera
while True: # main game loop
....
    # draw blackground on moving objects (hero, platforms, enemies)
    # (to delete just them on each frame instead of redrawing the entire screen,
    # as this will increase game performance)
    screen.blit(level.background, hero.rect, hero.rect) # delete hero
    for block in level.block_group:
        screen.blit(level.background, block.rect, block.rect) # delete other objects

    # check for hero moves nad collision
    hero.movement(event)
    hero.collision(level.block_group)

    camera.follow_target(hero) # centers the hero to the camera Rect

    # update blocks pos according to hero pos
    for block in  level.block_group:
        camera.apply_shift(block)
        screen.blit(block.image, block.rect)

    # draw hero
    screen.blit(hero.image, hero.rect)

    pygame.display.update()
\$\endgroup\$
0
\$\begingroup\$

Ok so the solution was pretty simple, but finding it out was a pain!.... It appears that the line of code in the follow_target method within the Camera class had a bug: the attributes self.dx and self.dy, indicating the shift of the camera according to the movements oh the hero, only accounted for hero's hspeed at each frame, and not the actual total shift. This led the hero to surpass the screen center as he kept moving.

Also, the position of the hero himself had to shift together with the blocks (and with all level objects in general) for the game to work properly.

The change in the Camera class was (note the += operand instead of =):

# calculate dx, dy based on previous and current camera position
self.dx += self.x - self.rect.x
self.dy += self.y - self.rect.y

Only working for right movements for now, but I'll keep this answer up to date as I update the code.

The change in the main function was to add a new Sprite Group (objects), adding to it both the hero and the blocks sprites, and update everything at each frame like:

objects = pygame.sprite.Group() # initialize sprite Group
objects.add(hero) # add hero to sprite Group (because he also has to shift together with other levels' objects)
objects.add(level.block_group) # add blocks to sprite Group

    while True: # main game loop
        ...
        # update every objects (including hero) pos according to hero pos
        for obj in  objects:
            camera.apply_shift(obj)
            screen.blit(obj.image, obj.rect)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.