====== 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. {{ :nsi:tds:pygame:plateforme:plateformes_scrolling.svg?800x280 |}} À 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 : * doter tous les sprites de deux ensembles de coordonnées. ''rect'' pour la position à l'écran et ''(x,y)'' pour la position relativement à la scène de jeu. * modifier la fonction ''draw'' utilisée pour tenir compte du décalage de la caméra. 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 : * on met à jour la position du joueur selon son mouvement, * on ajuste la caméra selon la nouvelle position du joueur, * on dessine les sprites en utilisant le ''draw'' de la caméra afin de tenir compte de l'offset de cette caméra. 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 [[nsi:terminales:programmation_fonctionnelle:presentation|programmation fonctionnelle]]. >>> t = [1, 2, 3, 4, 5] >>> map(lambda x:x**2, t) >>> 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 ''''. 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) ...