Aller au contenu

Tilemaps et scrolling en Pygame

Créez des niveaux complets comme dans les vrais jeux 2D en utilisant des tilemaps et une caméra avec scrolling en Pygame.

Jusqu’ici, vos niveaux étaient codés à la main, avec des rectangles placés directement dans le code. Ça fonctionne, mais dès qu’un niveau devient un peu grand, ça devient vite pénible.

Dans cette page, on va voir comment :

  • construire un niveau à partir d’une tilemap
  • faire un monde plus grand que l’écran
  • ajouter une caméra (scrolling) qui suit le joueur

Qu’est-ce qu’une tilemap ?

Une tilemap, c’est :

  • une grille
  • composée de tuiles (tiles)
  • toutes de la même taille

Chaque case de la grille représente un type de terrain.


Représenter une tilemap en Python

La méthode la plus simple est une liste de chaînes de caractères.

level = [
    "############################",
    "#............#.............#",
    "#.######.#####.#.#####.####.#",
    "#.#....#.....#.#.....#....#.#",
    "#.#.##.#####.#.#####.##.##.#.#",
    "#...##.......#.......##....#",
    "############################",
]

Ici :

  • # = mur
  • . = chemin libre

Taille des tiles

Toutes les tiles ont la même taille :

TILE_SIZE = 32

La position d’une tile (x, y) dans la grille devient :

pixel_x = x * TILE_SIZE
pixel_y = y * TILE_SIZE

Générer les murs depuis la tilemap

On parcourt la grille et on crée un Rect pour chaque mur.

walls = []

for y, row in enumerate(level):
    for x, tile in enumerate(row):
        if tile == "#":
            rect = pygame.Rect(
                x * TILE_SIZE,
                y * TILE_SIZE,
                TILE_SIZE,
                TILE_SIZE
            )
            walls.append(rect)

Un monde plus grand que l’écran

Le labyrinthe est plus large que la fenêtre. On ne peut pas tout afficher en même temps.

Il faut donc une caméra.


Principe du scrolling

On ne déplace pas l’écran. On décale l’affichage du monde par rapport au joueur.

camera_x = player.rect.centerx - SCREEN_WIDTH // 2
camera_x = max(0, camera_x)

Dessiner avec la caméra

Pour dessiner un objet du monde :

screen_x = rect.x - camera_x

Les collisions, elles, restent en coordonnées réelles.


Exemple complet : labyrinthe avec scrolling

Ce jeu :

  • génère un labyrinthe avec une tilemap
  • permet de déplacer un joueur bleu
  • empêche de traverser les murs
  • utilise une caméra horizontale simple

Code complet

import pygame

# --- CONFIG ---
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 320
TILE_SIZE = 32
FPS = 60

# --- LEVEL ---
level = [
    "############################",
    "#............#.............#",
    "#.######.#####.#.#####.#####",
    "#.#....#.......#.....#....##",
    "#.#.##.#####.#.#####.##.####",
    "#.#.##.##....#.......##....#",
    "#...##....##.#.......##....#",
    "#.####.#####...#####.##.##.#",
    "#......#####################",
    "############################",
]

LEVEL_WIDTH = len(level[0]) * TILE_SIZE

# INIT
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

# PLAYER
class Player:
    def __init__(self):
        self.rect = pygame.Rect(36, 36, 20, 20)
        self.speed = 4

    def move(self, dx, dy, walls):
        self.rect.x += dx
        for wall in walls:
            if self.rect.colliderect(wall):
                if dx > 0:
                    self.rect.right = wall.left
                if dx < 0:
                    self.rect.left = wall.right

        self.rect.y += dy
        for wall in walls:
            if self.rect.colliderect(wall):
                if dy > 0:
                    self.rect.bottom = wall.top
                if dy < 0:
                    self.rect.top = wall.bottom

    def update(self, keys, walls):
        dx = dy = 0
        if keys[pygame.K_LEFT]:
            dx = -self.speed
        if keys[pygame.K_RIGHT]:
            dx = self.speed
        if keys[pygame.K_UP]:
            dy = -self.speed
        if keys[pygame.K_DOWN]:
            dy = self.speed

        self.move(dx, dy, walls)

    def draw(self, screen, camera_x):
        pygame.draw.rect(
            screen,
            (0, 100, 255),
            self.rect.move(-camera_x, 0)
        )

# WALLS
walls = []
for y, row in enumerate(level):
    for x, tile in enumerate(row):
        if tile == "#":
            walls.append(
                pygame.Rect(
                    x * TILE_SIZE,
                    y * TILE_SIZE,
                    TILE_SIZE,
                    TILE_SIZE
                )
            )

player = Player()

# GAME LOOP
running = True
while running:
    clock.tick(FPS)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    player.update(keys, walls)

    # caméra
    camera_x = player.rect.centerx - SCREEN_WIDTH // 2
    camera_x = max(0, min(camera_x, LEVEL_WIDTH - SCREEN_WIDTH))

    # draw
    screen.fill((30, 30, 30))

    for wall in walls:
        pygame.draw.rect(
            screen,
            (200, 200, 200),
            wall.move(-camera_x, 0)
        )

    player.draw(screen, camera_x)

    pygame.display.flip()

pygame.quit()

Quizz

wall.move(-camera_x, 0)
Pourquoi applique-t-on `-camera_x` uniquement à l’affichage ?
- Pour déplacer le joueur
- *Pour décaler l’affichage sans casser les collisions
- Pour gérer la gravité
- Pour accélérer le jeu
> La caméra sert uniquement à l’affichage. La logique du jeu reste en coordonnées réelles.

Exercice pratique

Modifiez votre Mario de manière à lui ajouter une tilemap.


En résumé

Vous savez maintenant :

  • Créer un niveau avec une tilemap
  • Générer automatiquement les murs
  • Implémenter un scrolling simple
  • Gérer des collisions dans un monde plus grand que l’écran
  • Séparer logique du jeu et affichage

Avec ça, vous avez les bases utilisées dans la majorité des jeux 2D professionnels.