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 ... caseet 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 :
- rendre explicite les relations entre les types d’objets. Le code est modulaire et lisible. La responsabilité de chaque composant est claire.
- regrouper les données (les champs) et les fonctions (les méthodes) qui manipulent ces données
- 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 :
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 .....
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