Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Programmation objet

La programmation objet est une façon d’organiser son code. Elle consiste à placer les données (des struct) et les fonctions sur ces données (les méthodes) ensemble. Ce n’est pas une recette miracle, mais elle résout des problèmes de maintenabilité dans certains cas.

Si la programmation objet n’existait pas…

Si la programmation objet n’existait pas, il faudrait l’inventer ! Dans un jeu vidéo, on a besoin de gérer des ennemis différents : gobelins, poissons méchants, fantômes, etc. Si la programmation objet n’existait pas, on pourrait avoir un code qui ressemble à ceci.

function update(Enemy* enemy) {
    switch(enemy->type) {
        case gobelin: ... break;
        case fish: ... break;
        case ghost: ... break;
    }
}
function move(Enemy* enemy, Vector2* direction) {
    switch(enemy->type) {
        case gobelin: ... break;
        case fish: ... break;
        case ghost: ... break;
    }
}




function draw(Enemy* enemy, Screen* screen) {
    switch(enemy->type) {
        case gobelin: ... break;
        case fish: ... break;
        case ghost: ... break;
    }
}

Inconvénients :

  • Pas de polymorphisme. Pour afficher un enemi, on est obligé explicitement d’écrire un switch ... case et faire une étude de cas sur tous les types d’enemis possibles. En fonction du type, on exécute la bonne fonction. Le code est lourdingue.
  • Difficile à étendre. Pour ajouter un nouveau type d’ennemi, il faut modifier toutes ces fonctions (et donc tous les fichiers sont modifiés, donc il faut de nouveau avoir confiance en tous les fichiers modifiés)
  • Code peu lisible. L’éparpillement des fonctions rend la lecture du code difficile à comprendre pour une personne qui ne connaît pas le projet. C’est un code spaghetti.
  • Pas de sécurité. On peut modifier n’importe quel champ n’importe quand n’importe où. Ce n’est pas sécurisé.

Exemple de mauvais code

import sys
import math
import random
import pygame

# --- CONFIGURATION INITIALE ---
WIDTH, HEIGHT = 800, 600
GRID_SIZE = 40
FPS = 60

# Couleurs (style NES)
COLOR_BG = (100, 160, 60)          # Herbe / Sol
COLOR_WALL = (160, 100, 40)        # Mur / Rocher
COLOR_PLAYER = (50, 180, 50)       # Link (Vert)
COLOR_OCTO = (200, 50, 50)         # Octorok (Rouge)
COLOR_MOBLIN = (40, 80, 220)       # Moblin (Bleu)
COLOR_SWORD = (220, 220, 255)      # Épée
COLOR_HEART = (220, 40, 40)        # Cœur
COLOR_TEXT = (255, 255, 255)       # Texte
COLOR_UI_BG = (0, 0, 0)            # Fond d'interface

# Carte du monde (1 = Mur, 0 = Vide)
TILE_MAP = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1],
    [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

# --- CREATION DE L'ETAT DU JEU ---
def create_game_state():
    return {
        "player_x": 100.0,
        "player_y": 100.0,
        "player_speed": 3.0,
        "player_hp": 2,
        "player_max_hp": 3,
        "direction": "down",
        "is_attacking": False,
        "attack_timer": 0,
        "invulnerable_timer": 0,
        "score": 0,
        "enemies": [
            # Type: "octorok" (Rouge, lent, 1 PV)
            {"type": "octorok", "x": 300.0, "y": 200.0, "hp": 1, "speed": 1.5, "dir_x": 1, "dir_y": 0, "change_dir_timer": 0},
            {"type": "octorok", "x": 200.0, "y": 450.0, "hp": 1, "speed": 1.5, "dir_x": -1, "dir_y": 0, "change_dir_timer": 0},
            # Type: "moblin" (Bleu, rapide, 3 PV, chasseur)
            {"type": "moblin", "x": 500.0, "y": 300.0, "hp": 3, "speed": 2.2, "dir_x": 0, "dir_y": 1, "change_dir_timer": 0},
            {"type": "moblin", "x": 600.0, "y": 150.0, "hp": 3, "speed": 2.2, "dir_x": 0, "dir_y": -1, "change_dir_timer": 0},
        ],
        "dropped_items": [
            {"type": "heart", "x": 180.0, "y": 100.0}
        ]
    }

# --- FONCTIONS DE LOGIQUE ---
def get_wall_rects():
    walls = []
    for row_idx, row in enumerate(TILE_MAP):
        for col_idx, tile in enumerate(row):
            if tile == 1:
                walls.append(pygame.Rect(col_idx * GRID_SIZE, row_idx * GRID_SIZE, GRID_SIZE, GRID_SIZE))
    return walls

def check_collision(rect, wall_rects):
    for wall in wall_rects:
        if rect.colliderect(wall):
            return True
    return False

def update_player(state, keys, wall_rects):
    if state["player_hp"] <= 0:
        return

    dx, dy = 0, 0
    if keys[pygame.K_LEFT] or keys[pygame.K_q] or keys[pygame.K_a]:
        dx -= 1
        state["direction"] = "left"
    elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
        dx += 1
        state["direction"] = "right"
    
    if keys[pygame.K_UP] or keys[pygame.K_z] or keys[pygame.K_w]:
        dy -= 1
        state["direction"] = "up"
    elif keys[pygame.K_DOWN] or keys[pygame.K_s]:
        dy += 1
        state["direction"] = "down"

    if dx != 0 and dy != 0:
        dx *= 0.7071
        dy *= 0.7071

    new_x = state["player_x"] + dx * state["player_speed"]
    player_rect = pygame.Rect(new_x, state["player_y"], 30, 30)
    if not check_collision(player_rect, wall_rects):
        state["player_x"] = new_x

    new_y = state["player_y"] + dy * state["player_speed"]
    player_rect = pygame.Rect(state["player_x"], new_y, 30, 30)
    if not check_collision(player_rect, wall_rects):
        state["player_y"] = new_y

    if keys[pygame.K_SPACE] and not state["is_attacking"]:
        state["is_attacking"] = True
        state["attack_timer"] = 12

    if state["is_attacking"]:
        state["attack_timer"] -= 1
        if state["attack_timer"] <= 0:
            state["is_attacking"] = False

    if state["invulnerable_timer"] > 0:
        state["invulnerable_timer"] -= 1

def get_sword_rect(state):
    px, py = state["player_x"], state["player_y"]
    direction = state["direction"]
    if direction == "up":
        return pygame.Rect(px + 5, py - 25, 20, 25)
    elif direction == "down":
        return pygame.Rect(px + 5, py + 30, 20, 25)
    elif direction == "left":
        return pygame.Rect(px - 25, py + 5, 25, 20)
    elif direction == "right":
        return pygame.Rect(px + 30, py + 5, 25, 20)
    return pygame.Rect(0, 0, 0, 0)

def update_enemies(state, wall_rects):
    player_rect = pygame.Rect(state["player_x"], state["player_y"], 30, 30)
    sword_rect = get_sword_rect(state) if state["is_attacking"] else None

    for enemy in state["enemies"][:]:
        # IA spécifique selon le type d'ennemi
        if enemy["type"] == "octorok":
            # Déplacement aléatoire classique
            enemy["change_dir_timer"] -= 1
            if enemy["change_dir_timer"] <= 0:
                enemy["dir_x"] = random.choice([-1, 0, 1])
                enemy["dir_y"] = random.choice([-1, 0, 1])
                enemy["change_dir_timer"] = random.randint(30, 90)

        elif enemy["type"] == "moblin":
            # Traque le joueur s'il est proche (distance < 200 pixels)
            dist_x = state["player_x"] - enemy["x"]
            dist_y = state["player_y"] - enemy["y"]
            dist = math.hypot(dist_x, dist_y)

            if dist < 200 and dist > 0:
                enemy["dir_x"] = dist_x / dist
                enemy["dir_y"] = dist_y / dist
            else:
                enemy["change_dir_timer"] -= 1
                if enemy["change_dir_timer"] <= 0:
                    enemy["dir_x"] = random.choice([-1, 0, 1])
                    enemy["dir_y"] = random.choice([-1, 0, 1])
                    enemy["change_dir_timer"] = random.randint(30, 90)

        # Mouvement de l'ennemi
        new_x = enemy["x"] + enemy["dir_x"] * enemy["speed"]
        new_y = enemy["y"] + enemy["dir_y"] * enemy["speed"]
        enemy_rect = pygame.Rect(new_x, new_y, 30, 30)

        if not check_collision(enemy_rect, wall_rects):
            enemy["x"], enemy["y"] = new_x, new_y
        else:
            enemy["change_dir_timer"] = 0

        # Collision Épée / Ennemi
        if sword_rect and sword_rect.colliderect(enemy_rect):
            enemy["hp"] -= 1
            # Recul de l'ennemi suite au coup
            enemy["x"] += enemy["dir_x"] * -25
            enemy["y"] += enemy["dir_y"] * -25
            
            if enemy["hp"] <= 0:
                # Droprate selon l'ennemi (le Moblin donne toujours un cœur)
                drop_chance = 0.4 if enemy["type"] == "octorok" else 1.0
                if random.random() < drop_chance:
                    state["dropped_items"].append({
                        "type": "heart",
                        "x": enemy["x"] + 5,
                        "y": enemy["y"] + 5
                    })
                points = 100 if enemy["type"] == "octorok" else 250
                state["enemies"].remove(enemy)
                state["score"] += points
                continue

        # Collision Joueur / Ennemi
        if player_rect.colliderect(enemy_rect) and state["invulnerable_timer"] <= 0:
            state["player_hp"] -= 1
            state["invulnerable_timer"] = 40

def update_items(state):
    player_rect = pygame.Rect(state["player_x"], state["player_y"], 30, 30)

    for item in state["dropped_items"][:]:
        item_rect = pygame.Rect(item["x"], item["y"], 16, 16)
        if player_rect.colliderect(item_rect):
            if item["type"] == "heart":
                if state["player_hp"] < state["player_max_hp"]:
                    state["player_hp"] += 1
                state["score"] += 20
                state["dropped_items"].remove(item)

# --- FONCTIONS DE RENDU ---
def draw_game(screen, font, state, wall_rects):
    screen.fill(COLOR_BG)

    # Dessin de la carte (murs)
    for wall in wall_rects:
        pygame.draw.rect(screen, COLOR_WALL, wall)
        pygame.draw.rect(screen, (100, 60, 20), wall, 2)

    # Dessin des items au sol (Cœurs)
    for item in state["dropped_items"]:
        if item["type"] == "heart":
            ix, iy = item["x"], item["y"]
            pygame.draw.rect(screen, COLOR_HEART, (ix, iy, 16, 16))
            pygame.draw.rect(screen, (255, 255, 255), (ix + 3, iy + 3, 4, 4))

    # Dessin des ennemis
    for enemy in state["enemies"]:
        color = COLOR_OCTO if enemy["type"] == "octorok" else COLOR_MOBLIN
        pygame.draw.rect(screen, color, (enemy["x"], enemy["y"], 30, 30))
        
        # Dessin distinctif pour le Moblin (petites cornes/oreilles)
        if enemy["type"] == "moblin":
            pygame.draw.rect(screen, (200, 200, 200), (enemy["x"] + 2, enemy["y"] - 4, 6, 6))
            pygame.draw.rect(screen, (200, 200, 200), (enemy["x"] + 22, enemy["y"] - 4, 6, 6))

        # Yeux de l'ennemi
        pygame.draw.rect(screen, (255, 255, 255), (enemy["x"] + 5, enemy["y"] + 5, 5, 5))
        pygame.draw.rect(screen, (255, 255, 255), (enemy["x"] + 20, enemy["y"] + 5, 5, 5))

    # Dessin du joueur
    if state["player_hp"] > 0:
        if state["invulnerable_timer"] % 4 < 2:
            pygame.draw.rect(screen, COLOR_PLAYER, (state["player_x"], state["player_y"], 30, 30))
            px, py = state["player_x"], state["player_y"]
            if state["direction"] == "down":
                pygame.draw.rect(screen, (200, 200, 100), (px + 10, py + 20, 10, 10))
            elif state["direction"] == "up":
                pygame.draw.rect(screen, (200, 200, 100), (px + 10, py, 10, 10))
            elif state["direction"] == "left":
                pygame.draw.rect(screen, (200, 200, 100), (px, py + 10, 10, 10))
            elif state["direction"] == "right":
                pygame.draw.rect(screen, (200, 200, 100), (px + 20, py + 10, 10, 10))

    # Dessin de l'attaque à l'épée
    if state["is_attacking"]:
        sword_rect = get_sword_rect(state)
        pygame.draw.rect(screen, COLOR_SWORD, sword_rect)

    # --- BARRE D'INTERFACE (HUD) ---
    pygame.draw.rect(screen, COLOR_UI_BG, (0, 0, WIDTH, 40))
    
    # Vie (Cœurs de l'interface)
    for i in range(state["player_max_hp"]):
        c = COLOR_HEART if i < state["player_hp"] else (80, 80, 80)
        pygame.draw.rect(screen, c, (10 + i * 25, 10, 20, 20))

    # Score
    score_text = font.render(f"SCORE: {state['score']}", True, COLOR_TEXT)
    screen.blit(score_text, (WIDTH - 150, 10))

    # Fin de partie
    if state["player_hp"] <= 0:
        msg = font.render("GAME OVER - Appuyez sur R pour rejouer", True, (255, 50, 50))
        screen.blit(msg, (WIDTH // 2 - 220, HEIGHT // 2))
    elif len(state["enemies"]) == 0:
        msg = font.render("VICTOIRE ! Appuyez sur R pour rejouer", True, (255, 255, 100))
        screen.blit(msg, (WIDTH // 2 - 200, HEIGHT // 2))

# --- BOUCLE PRINCIPALE ---
def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Zelda 1 - Octoroks & Moblins")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont("monospace", 20, bold=True)

    state = create_game_state()
    wall_rects = get_wall_rects()

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    state = create_game_state()

        keys = pygame.key.get_pressed()
        update_player(state, keys, wall_rects)
        update_enemies(state, wall_rects)
        update_items(state)

        draw_game(screen, font, state, wall_rects)

        pygame.display.flip()
        clock.tick(FPS)

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

Solution : programmation objet

La solution est de :

  1. rendre explicite les relations entre les types d’objets. Le code est modulaire et lisible. La responsabilité de chaque composant est claire.
  2. regrouper les données (les champs) et les fonctions (les méthodes) qui manipulent ces données
  3. cacher les détails techniques. On parle d’encapsulation.

On peut faire de la “programmation objet” en C… mais c’est très fastidieux. Il y a des langages de programmation adaptés à la programmation objet (quasiment tous : C++, C#, Java, Javascript, Typescript, OCaml, Ruby, Kotlin, etc.). Un langage orienté objet offre du sucre syntaxique pour réaliser les points 1-3.

Un gobelin est un ennemi qui
- a une arme
- bouge en marchant 
- s'affiche avec l'image gobelin.png

Un poisson méchant est un ennemi qui :
- a une bouche qui fait des bulles
- perd des points de vie en dehors de l'eau (mais c'est un détail interne)
- bouge en nageant
- s'affiche avec l'image poisson.png




Un fantôme est un ennemi qui :
- a un drap
- bouge en flottant
- s'affiche en transparence

Inspiration de la vraie vie

Un des succès de la programmation objet est que son approche est proche de la vraie vie. Il parle donc à beaucoup de développeurs.

Approche objet

Un chat est un mamifère :

  • (et donc a 4 pattes)
  • a des vibrisses
  • peut avancer
  • peut faire du bruit (miauler/parler)

Une voiture est un véhicule qui :

  • a un moteur (même s’il est plein de cambouis et qu’on ne veut pas le voir)
  • a une couleur
  • peut avancer
  • peut faire du bruit
  • peut freiner

Vous avez sans doute dans votre bibliothèque :

Je suis livre qui te dit tout sur les chats.

📖

Je suis livre qui te dit tout sur les voitures.

📖

Approche non objet

L’approche non objet reviendrait à avoir des livres qui t’expliquent comment “les trucs avancent”, “les trucs font du bruit” avec des switch... case .....

Je suis un livre qui te dit comment les trucs avancent. Si t’es un animal, va p. 10, si t’es une voiture, va p. 42…

📖

Méthodologie de la programmation objet

  • à définir les types d’objets. Un type d’objet s’appelle une classe
  • à définir les relations entre les classes :
    • est-un “hérite de” (héritage),
    • avoir-un/des “est composé de” (composition)
  • à définir ce que les objets peuvent faire : ce sont des fonctions que l’on appelle des méthodes
  • à choisir ce que l’on cache aux autres développeurs.es : l’encapsulation
  • à utiliser des design patterns “patrons de conception” pour que le logiciel ait une architecture facile pour ajouter de fonctionnalités

Dans les langages de programmation

  • Langage où TOUT est objet : Ruby, Python
  • Langage Web : Javascript, Typescript (les éléments du DOM sont des objets)
  • Et C++, Java, C#, OCaml, Swift, Kotlin
  • Partiellement : Rust