CHAPITRE 6
Compléter le modèle⚓︎
Il est temps de faire avancer notre serpent. Pour cela, nous aurons besoin :
- de gérer les événements clavier (flèches)
- d'ajouter au modèle de serpent un couple pour la direction : des valeurs -1, 0 et +1 à ajouter sur les coordonnées pour aller dans une des quatre directions.
Le serpent⚓︎
Nous ajoutons une propriété pour pouvoir sauvegarder les valeurs (0, 1) (le serpent se déplace vers le sud), (0, -1) il va vers le nord, (1, 0) il va vers l'est et (-1, 0) il va vers l'ouest. Au départ, il ne bouge pas : la direction de déplacement est initialisée à (0, 0).
class Serpent:
def __init__(self, arene):
...
self.direction = 0, 0
def change_direction(self, dx, dy):
self.direction = dx, dy
Les événements⚓︎
Dans la boucle de jeu, on attrape les événements correspondants aux flèches du clavier. Cette gestion peut se faire dans une méthode gerer_event
:
class Jeu:
...
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_DOWN:
self.arena.serpent_change_direction(0, 1)
elif event.key == pygame.K_UP:
self.arena.serpent_change_direction(0, -1)
elif event.key == pygame.K_LEFT:
self.arena.serpent_change_direction(-1, 0)
elif event.key == pygame.K_RIGHT:
self.arena.serpent_change_direction(1, 0)
Le code peut être encore amélioré en termes de lisibilité et de concision. Grâce à une structure appelée dictionnaire on peut associer à une valeur une clé. Ici l'idée serait donc d'associer à chacune des constantes pygame des touches fléchées le couple de changement de direction :
DIRECTIONS = {pygame.K_DOWN: (0, 1),
pyagme.K_UP: (0, -1),
pygame.K_LEFT: (-1, 0),
pygame.K_RIGHT: (1, 0)}
Les concepts
Les dictionnaires de Python
Ce qui nous donne une nouvelle version plus courte pour gerer_event
:
class Jeu:
...
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if event.key in DIRECTIONS:
dx, dy = DIRECTIONS[event.key]
self.arena.serpent_change_direction(dx, dy)
Et la boucle principale devient :
def loop(self):
fini = False
while not fini:
for event in pygame.event.get():
self.gerer_event(event)
self.arene.se_dessine()
pygame.display.flip()
pygame.quit()
L'arène ne fait que transmettre l'ordre au serpent :
class Arene:
...
def serpent_change_direction(self, dx, dy):
self.serpent.change_direction(dx, dy)
Bonne pratique
Même si certaines de vos méthodes ne font qu'une ou deux lignes, ne croyez pas que cela est superflus ou trop découpé. Dans un vrai projet, une grande modularité rendra les choses plus simples à suivre et facilitera le partage des tâches entre les différents développeurs.
Architecture ou design pattern⚓︎
Lors de la conception d'applications de taille conséquente, il existe des méthodes d'organisation du code : on parle de design pattern. L'un de ces design consiste à bien séparer ce qui concerne les affichages graphiques et les interactions avec l'utilisateur du modèle conceptuel du problème. Il s'agit du design Modèle-Vue-Contrôleur.
C'est en suivant un peu le principe MVC que nous avons structuré notre SNAKE :
Par rapport à notre SNAKE, vous reconnaissez en bleu (le contrôleur) notre classe Jeu
, en classe 1 du modèle l'Arene
et en classe 2 le Serpent
. Les parties grises Draw sont les méthodes et éventuellement les propriétés de nos objets pour permettre leur affichage. Il s'agit par exemple de la propriété ecran
et de la méthode se_dessine
de notre serpent.
Animer les objets⚓︎
Maintenant que le serpent sait changer de direction sur commande, il faut le faire bouger. Voyons sur les deux schémas ci-dessous un exemple où notre serpent avance vers le bas :
\(\quad\longrightarrow\quad\)
À gauche la position initiale, à droite la position après déplacement... nous constatons qu'il suffit de créer une nouvelle coordonnée en partant de celle de la tête et en y rajoutant les valeurs de la direction et de supprimer la queue du serpent. Ce qui donne :
class Serpent:
...
def bouge(self):
x, y = self.pos[self.tete]
dx, dy = self.direction
self.pos.append((x+dx, y+dy))
self.pos.pop(0)
Les concepts à revoir
Les listes de Python avec notamment les méthodes append
et pop
. On pourrait aussi, si ce projet est réaliser en terminale parler de la structure abstraite de file et utiliser une implémentation plus efficace.
Le jeu doit alors demander à l'arène d'animer son petit monde, dans sa boucle de jeu :
def loop(self):
fini = False
while not fini:
for event in pygame.event.get():
self.gerer_event(event)
self.arene.anime()
self.arene.se_dessine()
pygame.display.flip()
pygame.quit()
L'animation de l'arène se résume à demander au serpent de bouger :
class Arene:
...
def anime(self):
self.serpent.bouge()
Régler les soucis⚓︎
Si à ce stade vous testez le code obtenu vous constaterez :
- le serpent semble s'allonger démesurément
- ça va beaucoup trop vite
- on ne peut pas quitter le programme
Pour le point 2, il nous faut ajouter un délai entre 2 affichages. Pygame fournit ce qu'il faut :
pygame.time.delay(nb_de_ms)
Par exemple, avec 100 ms, nous avons une vitesse jouable. Une nouvelle constante à ajouter.
Le point 1 est réglé en nettoyant l'écran entre chaque affichage : en effet déplacer le serpent consiste simplement à le redessiner à sa nouvelle position, mais cela n'efface pas l'ancienne. Le plus simple est de repasser une couche de couleur de fond. Nous pourrions aussi réaliser quelque chose de moins brutal : demander au serpent de dessiner sa queue de la couleur du fond.
Enfin pour le point 3, il nous faut ajouter une propriété booléenne à notre objet Jeu
et basculer à True
si on appuie sur Q
par exemple.
Résumons ce qu'est devenu notre classe Jeu
:
La classe Jeu
class Jeu:
def __init__(self):
self.ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
self.arene = Arene(self)
self.fini = False
def start(self):
pygame.init()
pygame.display.set_caption('Another SNAKE game...')
self.ecran.fill(COULEUR_FOND)
self.loop()
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if event.key in DIRECTIONS:
dx, dy = DIRECTIONS[event.key]
self.arene.serpent_change_direction(dx, dy)
elif event.key == pygame.K_Q:
self.fini = True
def pause(self):
pygame.time.delay(DELAI_MS)
def effacer(self):
self.ecran.fill(COULEUR_FOND)
def loop(self):
while not self.fini:
for event in pygame.event.get():
self.gerer_event(event)
self.pause()
self.effacer()
self.arene.anime()
self.arene.se_dessine()
pygame.display.flip()
pygame.quit()
Exercice⚓︎
À faire vous-même
Réaliser les derniers changements dans un fichier snake_08.py
(pensez aussi à mettre à jour le fichier des constantes : les couleurs, les directions, le délai en millisecondes etc.)
Testez votre programme. Vous devriez pouvoir déplacer le serpent avec les touches fléchées et quitter le jeu avec la touche Q.
import pygame
from constantes import *
def xy_vers_pixels(coords):
x, y = coords
return x * SIZE, y * SIZE
class Serpent:
def __init__(self, arene):
self.arene = arene
self.ecran = arene.ecran
self.pos = [(COLS//2 + i, ROWS//2) for i in range(-LENGTH//2, LENGTH//2)]
self.tete = -1
self.direction = 0, 0
def change_direction(self, dx, dy):
self.direction = dx, dy
def bouge(self):
x, y = self.pos[self.tete]
dx, dy = self.direction
self.pos.append((x+dx, y+dy))
self.pos.pop(0)
def se_dessine(self):
for coords in self.pos:
px, py = xy_vers_pixels(coords)
pygame.draw.rect(self.ecran, COULEUR_CORPS, pygame.Rect(px, py, SIZE, SIZE))
px, py = xy_vers_pixels(self.pos[self.tete])
pygame.draw.rect(self.ecran, COULEUR_TETE, pygame.Rect(px, py, SIZE, SIZE))
class Arene:
def __init__(self, jeu):
self.jeu = jeu
self.ecran = jeu.ecran
self.serpent = Serpent(self)
def anime(self):
self.serpent.bouge()
def serpent_change_direction(self, dx, dy):
self.serpent.change_direction(dx, dy)
def se_dessine(self):
self.serpent.se_dessine()
class Jeu:
def __init__(self):
self.ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
self.arene = Arene(self)
self.fini = False
def start(self):
pygame.init()
pygame.display.set_caption('Another SNAKE game...')
self.ecran.fill(COULEUR_FOND)
self.loop()
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if event.key in DIRECTIONS:
dx, dy = DIRECTIONS[event.key]
self.arene.serpent_change_direction(dx, dy)
elif event.key == pygame.K_q:
self.fini = True
def pause(self):
pygame.time.delay(DELAI_MS)
def effacer(self):
self.ecran.fill(COULEUR_FOND)
def loop(self):
while not self.fini:
for event in pygame.event.get():
self.gerer_event(event)
self.pause()
self.effacer()
self.arene.anime()
self.arene.se_dessine()
pygame.display.flip()
pygame.quit()
snake_game = Jeu()
snake_game.start()
Le résultat de snake_08.py
Ce qu'il reste à faire...⚓︎
Vous constatez que pour l'instant il manque encore :
- les collisions (le serpent peut quitter l'arène sans problème)
- les pommes et tout ce qui va avec : le fait de pouvoir manger ces pommes et voir le serpent s'allonger etc.
- d'autres petites fonctionnalités : un score, un timer, ...
C'est ce que nous allons ajouter dans le prochain chapitre.