Scrolling dans le jeu de plateforme

On souhaite que la scène du jeu soit plus grande que l'écran. À mesure que le personnage se déplace, on découvre l'espace du jeu.


À présent, quand on indique les coordonnées d'un sprite (exemple le sprite en vert) il faut savoir s'il s'agit de coordonnées par rapport au bord de l'écran qui est affiché ou s'il s'agit de coordonnées par rapport au bord de la scène.

Tous les sprites ont attribut rect qui définit leur position. Si on utilise la fonction draw des groupes de sprites, rect sert à définir la position du sprite à l'écran. On a donc deux options :

Après avoir tenté la première option, je passe à la seconde option (c'est ce que m'a montré l'un d'entre vous qui m'a fait comprendre que c'était mieux !)

Camera

Je propose donc de créer un objet Camera dont le but est de contenir les informations sur le cadre et de prendre en charge la nouvelle fonction draw :

class Camera:
    def __init__(self, originex:int, originey:int, zone):
        """
        originex, originey: position initiale du cadre
        zone: (left, right, top, bottom)
              généralement on veut que la caméra suive le joueur.
              on précise alors dans quel zone de l'écran il doit rester.
              Si le joueur tend à en sortir, on ajuste l'écran
        """
        self.x = originex
        self.y = originey
        self.left, self.right, self.top, self.bottom = zone
    
    def adjust(self, px, py):
        """
        px, py: coordonnées du joueur par rapport à la scène
        selon les coordonnées px, py fournies
        réajuste la position de la caméra
        """
        if px < self.left + self.x:
            # il faut décaler l'écran vers la gauche
            self.x = px - self.left
        elif px > self.right + self.x:
            # il faut décaler l'écran vers la droite
            self.x = px - self.right

        if py < self.top + self.y:
            # il faut décaler l'écran vers le haut
            self.y = py - self.top
        elif py > self.bottom + self.y:
            # il faut décaler l'écran vers la droite
            self.y = py - self.bottom

    def draw_sprite(self, surface, sprite):
        """
        surface: objet Surface sur lequel imprimer le sprite
        sprite: objet Sprite à dessiner
        dessine un sprite sur la surface tenant compte de l'offset
        renvoie le rect de ce qui a été dessiné
        """
        # on calcule la position (x,y) du sprite à l'écran
        # tenant compte de :
        #   - sa position dans la scène (sprite.rect)
        #   - de la position de la caméra (self.x, self.y)
        x = sprite.rect.left - self.x
        y = sprite.rect.top - self.y
        return surface.blit(sprite.image, (x,y))

    def draw(self, surface, group):
        """
        surface: objet Surface sur lequel imprimer les sprites
        group: groupe de sprites
        dessine les sprites du groupe sur une surface
        """
        for sprite in group:
            self.draw_sprite(surface, sprite)

Modification du player

Puisque la caméra se déplace, cela n'a plus de sens de contraindre la position de player entre un xmin et un xmax. On pourra donc enlever cette configuration de player.

Modification du main

# main.py

...

from camera import Camera
...

cam = Camera(0, 0, (100, 700, 0, 600))
# signifie que la caméra est initialisée en haut à gauche du niveau
# et que la caméra bougera pour que player ne soit jamais
# en dehors de la zone délimitée par (100, 700, 0, 600)
# donc si en allant à droite, le joueur dépasse 700, la caméra le suit.

...
while running:
    clock.tick(30)
    screen.fill((0,0,0))

    player.update_position(platesformes_sprites)
    cam.adjust(player.rect.centerx, player.rect.centery)
    cam.draw(screen, all_sprites)
...

Dans la boucle :

Certains sprites pourraient ne pas être soumis au mouvement de caméra. Par exemple le hud. Ces sprites ne devront alors pas être soumis à cam.draw_sprite. On devrait d'ailleurs renommer all_sprites en un nom qui exprime clairement que les sprites du groupe sont soumis au mouvement de caméra.

optimisation

Dans Camera.draw, la boucle for peut occasionner des lenteurs. Il faut dire que les boucles for de Python sont lentes.

On peut ruser en utilisant une méthode qui utilise une boucle, mais une boucle cachée et plus rapide : on peut utiliser la fonction map typique de la programmation fonctionnelle.

>>> t = [1, 2, 3, 4, 5]
>>> map(lambda x:x**2, t)
<map object at 0x0000028BA8B6D700>
>>> list(map(lambda x:x**2, t))
[1, 4, 9, 16, 25]

Avec map, on applique une fonction à tous les éléments d'un itérable comme une list ou un groupe de sprites. Dans l'exemple ci-dessus, la fonction est $x \mapsto x^2$. Dans notre exemple, la fonction serait plutôt la fonction consistant à dessiner un sprite.

Vous remarquez que map tout seul n'a pas produit le résultat désiré. Il a produit un objet de type <map>. En gros, map renvoie un objet prêt à travailler mais qui n'a pas encore fait le travail… la conversion en list l'oblige à faire le travail.

# camera.py

class Camera:
    ...
    
    def draw(self, surface, group):
        """
        surface: objet Surface sur lequel imprimer les sprites
        group: groupe de sprites
        dessine les sprites du groupe sur une surface
        """
        list(map(lambda sprite:self.draw_sprite(surface, sprite), group))

Héritage

On peut considérer que Camera est un groupe de sprite avec une méthode draw adaptée.

# cameragroup.py

import pygame

class CameraGroup(pygame.sprite.Group):
    def __init__(self, originex:int, originey:int, zone):
        """
        originex, originey: position initiale du cadre
        zone: (left, right, top, bottom)
              généralement on veut que la caméra suive le joueur.
              on précise alors dans quel zone de l'écran il doit rester.
              Si le joueur tend à en sortir, on ajuste l'écran
        """
        super().__init__()
        self.x = originex
        self.y = originey
        self.left, self.right, self.top, self.bottom = zone

    ...
    def draw(self, surface)
        """
        surface: objet Surface sur lequel imprimer les sprites
        dessine les sprites du groupe sur une surface
        """
        list(map(lambda sprite:self.draw_sprite(surface, sprite), self))

Remarquez en dernière ligne que le group a été remplacé par self car maintenant la caméra est le groupe de sprites.

Dans le main :

# main.py

...
from cameragroup import CameraGroup
...
cam_group = Camera(0, 0, (200, 600, 150, 450))
...
cam_group.add(platesformes, decors, coins, player)
...
while running:
    ...
    cam_group.adjust(player.rect.centerx, player.rect.centery)
    cam_group.draw(screen)
    ...