CHAPITRE 8
Finaliser le jeu⚓︎
- Gérer les collisions
- Ajouter un score
- Ajouter un timer
Le concept de mode⚓︎
Lorsque le serpent entre en collision avec un des bords de l'arène ou avec lui-même, le jeu prend fin. Jusqu'à présent nous ne pouvons quitter le jeu qu'en utilisant la touche Q
de notre clavier. Même le bouton de fermeture de la fenêtre est inactif.
Si le jeu se termine à cause d'une collision, il n'est pas souhaitable que tout s'arrête brutalement, avec fermeture de la fenêtre. En effet, le joueur voudra probablement voir son score, éventuellement avoir accès à un menu pour recommencer une partie etc.
Basculer dans un mode stop serait approprié. Dans ce mode on pourrait avoir l'affichage d'un menu par exemple. On en tout cas, dans sa version la plus simple, le mode stop attend tout simplement que l'utilisateur décide de quitter le jeu.
Différents modes⚓︎
Nous pourrions profiter de ce concept pour créer un mode start qui serait celui dans lequel le jeu démarrerait, empêchant ainsi le serpent de commencer à se mouvoir sans l'intervention du joueur (vous avez remarqué que le serpent se replie sur lui-même au tout début ?), empêchant le timer de se lancer, etc.
Un mode pause pourrait être activé pendant la partie sur décision du joueur (un appui sur la barre espace
par exemple) : dans ce mode, le serpent ne bouge pas mais le timer continue de défiler
Le mode move ou mode normal serait celui de la partie qui se déroule, avec le serpent qui bouge.
Enfin le mode stop que nous avons déjà évoqué.
Ajout d'un mode au Jeu
⚓︎
Les constantes de modes sont :
MODE_START = 0 # le serpent ne bouge pas
MODE_MOVE = 1 # mode normal, le serpent bouge
MODE_PAUSE = 2 # le joueur a mis le jeu en pause
MODE_STOP = 3
Et l'impact sur le contrôleur :
class Jeu:
def __init__(self):
...
self.mode = MODE_START
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if self.mode == MODE_START:
self.debut = int(time.time())
self.mode = MODE_MOVE
if self.mode != MODE_STOP:
self.move_events(event)
elif event.key == pygame.K_q:
self.fini = True
elif # ici une condition pour savoir qu'un crash a eu lieu
and self.mode == MODE_MOVE:
self.game_over()
self.mode = MODE_STOP
elif event.type == pygame.QUIT:
self.fini = True
Les collisions⚓︎
Dès que le serpent a bougé, il doit vérifier qu'il n'a pas tenté de sortir de l'arène, ni heurté son propre corps. Si c'est le cas, il faut avertir le jeu (le contrôleur) que la partie est terminée (basculer en mode stop).
Le souci : le Serpent ne connaît que l'Arène, pas le Jeu. Alors nous pourrions faire une succession de renvois de valeur pour signifier au Jeu, par l'intermédiaire de l'Arène que la partie est finie.
Cette façon de faire est artificielle et surtout, pourrait être très fastidieuse si les intermédiaires entre l'objet qui doit faire l'annonce et le contrôleur s'avéraient nombreux (ce qui peut être le cas dans des projets plus importants).
Les événements utilisateur sont là pour ça.
Les événements utilisateur⚓︎
Lorsque le contrôleur teste le type d'un événement avec event.type == pygame.KEYDOWN
, la constante pygame.KEYDOWN
est un simple numéro identifiant un événement prédéfini ; ici le fait qu'une touche du clavier a été enfoncée.
Il existe une constante pygame.USEREVENT
qui est un identifiant libre, que l'on peut utiliser. Si on a besoin d'un deuxième événement utilisateur, il suffit d'incrémenter cette valeur : pygame.USEREVENT + 1
.
Ainsi, dans les constantes nous allons déclarer un événement crash :
CRASH_EVENT = pygame.USERVENT
Ensuite, il suffira au serpent de déclencher cet événement lorsqu'il aura détecté une collision. Avant de détailler cela, nous pouvons compléter la partie manquante de notre méthode gerer_event
:
class Jeu:
...
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if self.mode == MODE_START:
self.debut = int(time.time())
self.mode = MODE_MOVE
if self.mode != MODE_STOP:
self.move_events(event)
elif event.key == pygame.K_q:
self.fini = True
elif event.type == CRASH_EVENT and self.mode == MODE_MOVE:
self.game_over()
self.mode = MODE_STOP
elif event.type == pygame.QUIT:
self.fini = True
Note
La méthode game_over
pourra afficher un message au milieu de l'écran et attendre que le joueur quitte le jeu.
Du côté du Serpent⚓︎
Il faut déjà vérifier la collision éventuelle :
Le serpent se mord ?⚓︎
class Serpent:
...
def se_mord(self):
return any(self.pos[i] == self.pos[self.tete]
for i in range(len(self.pos)-1))
Les concepts
- Les itérateurs
- Les fonctions qui les utilisent :
sum
,max
,sorted
,all
,any
...
Le serpent est dans l'arène ?⚓︎
class Serpent:
...
def dans_arene(self):
"""Renvoie True si le serpent est bien dans l'arène"""
tete_x, tete_y = self.pos[self.tete]
return 0 <= tete_x < COLS and 0 <= tete_y < ROWS
Le crash ?⚓︎
class Serpent:
...
def crash(self):
"""Renvoie True si le serpent s'est crashé qq part"""
if self.se_mord() or not self.dans_arene():
self.declenche_event(CRASH_EVENT)
return True
return False
Le déclenchement d'un event se fait en deux temps :
- création d'un objet événement avec le bon identifiant :
pygame.event.Event(event_id)
- mettre l'événement dans le buffer des événements :
pygame.event.post(event)
class Serpent:
...
def declencher_event(self, event_id):
pygame.event.post(pygame.event.Event(event_id))
Exercice⚓︎
À faire vous-même
Intégrer la gestion des crashes dans une version snake_10.py
. Pour le game_over
pour l'instant fait juste un pass
import pygame
import random
import time
from constantes import *
def xy_vers_pixels(coords):
x, y = coords
return x * SIZE, y * SIZE
class Pomme:
def __init__(self, pos, arene):
self.arene = arene
self.ecran = arene.ecran
self.pos = pos
def se_dessine(self):
px, py = xy_vers_pixels(self.pos)
pygame.draw.rect(self.ecran, COULEUR_POMME, pygame.Rect(px, py, SIZE, 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 mange(self):
pos_queue = self.pos[0]
for _ in range(CROISSANCE):
self.pos.insert(0, pos_queue)
def declenche_event(self, event_id):
pygame.event.post(pygame.event.Event(event_id))
def se_mord(self):
"""Renvoie True si la position de la tête se retrouve à une des positions du corps, False sinon"""
return any(self.pos[i] == self.pos[self.tete] for i in range(len(self.pos)-1))
def dans_arene(self):
"""Renvoie True si le serpent est bien dans l'arène"""
tete_x, tete_y = self.pos[self.tete]
return 0 <= tete_x < COLS and 0 <= tete_y < ROWS
def crash(self):
"""Renvoie True si le serpent s'est crashé qq part"""
if self.se_mord() or not self.dans_arene():
self.declenche_event(CRASH_EVENT)
return True
return False
def bouge(self):
x, y = self.pos[self.tete]
dx, dy = self.direction
self.pos.append((x+dx, y+dy))
self.pos.pop(0)
if not self.crash():
if self.arene.une_pomme():
self.mange()
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)
self.pommes = {}
self.date = int(time.time())
def anime(self):
self.gestion_pommes()
self.serpent.bouge()
def serpent_change_direction(self, dx, dy):
self.serpent.change_direction(dx, dy)
def une_pomme(self):
position_serpent = self.serpent.pos[self.serpent.tete]
if position_serpent in self.pommes:
self.pommes.pop(position_serpent)
return True
return False
def random_position(self):
return random.randrange(COLS), random.randrange(ROWS)
def ajoute_pomme(self):
pos = self.random_position()
self.pommes[pos] = Pomme(pos, self)
def gestion_pommes(self):
date = int(time.time())
if date - self.date > DELAI_POMME:
self.ajoute_pomme()
self.date = date
def se_dessine(self):
self.serpent.se_dessine()
for pomme in self.pommes.values():
pomme.se_dessine()
class Jeu:
def __init__(self):
self.ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
self.arene = Arene(self)
self.fini = False
self.mode = MODE_START
def start(self):
pygame.init()
pygame.display.set_caption('Another SNAKE game...')
self.ecran.fill(COULEUR_FOND)
self.loop()
def move_events(self, event):
if event.key in DIRECTIONS:
dx, dy = DIRECTIONS[event.key]
self.arene.serpent_change_direction(dx, dy)
elif event.key == pygame.K_SPACE:
self.mode = 3 - self.mode # bascule de PAUSE à MOVE et vice-versa
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if self.mode == MODE_START:
self.debut = int(time.time())
self.mode = MODE_MOVE
if self.mode != MODE_STOP:
self.move_events(event)
elif event.key == pygame.K_q:
self.fini = True
elif event.type == CRASH_EVENT and self.mode == MODE_MOVE:
self.game_over()
self.mode = MODE_STOP
elif event.type == pygame.QUIT:
self.fini = True
def game_over(self):
pass
def effacer(self):
self.ecran.fill(COULEUR_FOND)
def refresh_and_pause(self):
pygame.display.flip()
pygame.time.delay(DELAI_MS)
def loop(self):
while not self.fini:
for event in pygame.event.get():
self.gerer_event(event)
if self.mode != MODE_STOP:
self.effacer()
if self.mode == MODE_MOVE:
self.arene.anime()
self.arene.se_dessine()
self.refresh_and_pause()
pygame.quit()
snake_game = Jeu()
snake_game.start()
Ajout d'un score⚓︎
En nous servant du principe de l'événement utilisateur, nous allons ajouter un score au jeu : propriété initialisée à 0, à chaque pomme mangée, ce score augmente de 1.
Il nous faut utiliser les objets texte de pygame pour afficher des informations à l'écran.
Les textes de Pygame⚓︎
Probablement un des points faibles de Pygame1. Pour afficher un texte il faut :
- Définir une fonte,
- faire un rendu de cette fonte sur un texte : ce qui donne une image
- afficher cette image
Heureusement, il est possible d'utiliser des valeurs par défaut rendant le processus un peu moins pénible. L'étape 1 se fait au démarrage, avec une fonte par défaut présente sur le système (c'est la signification de l'argument None
) :
class Jeu:
...
def start(self):
pygame.init()
self.font = pygame.font.SysFont(None, 24)
...
Les étapes 2 et 3 se font au moment de l'affichage :
class Jeu:
...
def infos(self):
score = self.font.render(f'{self.score:03}', True, COULEUR_TEXTE)
self.ecran.blit(score, POSITION_SCORE)
La méthode blit
permet d'afficher une image.
La propriété score
⚓︎
Elle fait partie des propriétés de l'objet Jeu
:
class Jeu:
...
def __init__(self):
...
self.score = 0
et la mise à jour se fait par événement utilisateur :
SCORE_EVENT = pygame.USEREVENT + 1
qui sera déclenché par le Serpent :
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)
if not self.crash():
if self.arene.une_pomme():
self.declencher_event(SCORE_EVENT)
self.mange()
et intercepté par le contrôleur :
class Jeu:
...
def gerer_event(self, event):
...
elif event.type == SCORE_EVENT:
self.score += 1
...
Exercice final⚓︎
À faire vous-même
Réaliser la version finale du SNAKE en intégrant :
- les modes et les collisions
- le score
- le timer
- on pourra aussi ajouter plsu d'aléatoire sur l'apparition des pommes, faire en sorte que les pommes disparaissent au bout d'un temps aléatoire aussi, faire accélérer régulièrement le serpent...
import pygame
import random
from constantes import *
def xy_vers_pixels(coords):
x, y = coords
return x * SIZE, y * SIZE
class Pomme:
def __init__(self, pos, arene, date_peremption):
self.arene = arene
self.ecran = arene.ecran
self.pos = pos
self.date_peremption = date_peremption
def se_dessine(self):
px, py = xy_vers_pixels(self.pos)
pygame.draw.rect(self.ecran, COULEUR_POMME, pygame.Rect(px, py, SIZE, 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 mange(self):
pos_queue = self.pos[0]
for _ in range(CROISSANCE):
self.pos.insert(0, pos_queue)
def declenche_event(self, event_id):
pygame.event.post(pygame.event.Event(event_id))
def se_mord(self):
"""Renvoie True si la position de la tête se retrouve à une des positions du corps, False sinon"""
return any(self.pos[i] == self.pos[self.tete] for i in range(len(self.pos)-1))
def dans_arene(self):
"""Renvoie True si le serpent est bien dans l'arène"""
tete_x, tete_y = self.pos[self.tete]
return 0 <= tete_x < COLS and 0 <= tete_y < ROWS
def crash(self):
"""Renvoie True si le serpent s'est crashé qq part"""
if self.se_mord() or not self.dans_arene():
self.declenche_event(CRASH_EVENT)
return True
return False
def bouge(self):
x, y = self.pos[self.tete]
dx, dy = self.direction
self.pos.append((x+dx, y+dy))
self.pos.pop(0)
if not self.crash():
if self.arene.une_pomme():
self.declenche_event(SCORE_EVENT)
self.mange()
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)
self.pommes = {}
self.date = jeu.time()
def anime(self):
self.gestion_pommes()
self.serpent.bouge()
def serpent_change_direction(self, dx, dy):
self.serpent.change_direction(dx, dy)
def retrait_pomme(self, pos):
self.pommes.pop(pos)
def ajout_pomme(self, date_peremption):
pos = self.random_position()
self.pommes[pos] = Pomme(pos, self, date_peremption)
def une_pomme(self):
position_serpent = self.serpent.pos[self.serpent.tete]
if position_serpent in self.pommes:
self.retrait_pomme(position_serpent)
return True
return False
def random_position(self):
return random.randrange(COLS), random.randrange(ROWS)
def retire_pommes(self):
"""retire les pommes périmées"""
date = self.jeu.time()
for pos in list(self.pommes.keys()):
pomme = self.pommes[pos]
if pomme.date_peremption < date:
self.retrait_pomme(pos)
def gestion_pommes(self):
date = self.jeu.time()
delai_apparition = random.randint(*DELAI_POMME)
delai_peremption = random.randint(*VALIDITE)
if (date - self.date) > delai_apparition:
self.ajout_pomme(date + delai_peremption)
self.date = date
self.retire_pommes()
def se_dessine(self):
self.serpent.se_dessine()
for pomme in self.pommes.values():
pomme.se_dessine()
def reset(self):
self.serpent = Serpent(self)
self.pommes = {}
self.date = self.jeu.time()
class Jeu:
def __init__(self):
self.ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
self.arene = Arene(self)
self.fini = False
self.mode = MODE_START
self.debut = None # sera initialisé au moment du lancement
self.score = 0
self.font = None
self.delai = DELAI_MS
self.deja_accelere = False
def start(self):
pygame.init()
pygame.display.set_caption('Another SNAKE game...')
self.font = pygame.font.SysFont(None, 24)
self.ecran.fill(COULEUR_FOND)
self.loop()
def move_events(self, event):
if event.key in DIRECTIONS:
dx, dy = DIRECTIONS[event.key]
self.arene.serpent_change_direction(dx, dy)
elif event.key == pygame.K_SPACE:
self.mode = 3 - self.mode # bascule de PAUSE à MOVE et vice-versa
elif event.key == pygame.K_q:
self.fini = True
def time(self):
# return int(time.time())
return pygame.time.get_ticks() // 1000
def gerer_event(self, event):
if event.type == pygame.KEYDOWN:
if self.mode == MODE_START:
self.debut = self.time()
self.mode = MODE_MOVE
if self.mode != MODE_STOP:
self.move_events(event)
elif event.key == pygame.K_q:
self.fini = True
elif event.key == pygame.K_r:
self.reset()
self.mode = MODE_START
elif event.type == SCORE_EVENT:
self.score += 1
elif event.type == CRASH_EVENT and self.mode == MODE_MOVE:
self.game_over()
self.mode = MODE_STOP
elif event.type == pygame.QUIT:
self.fini = True
def accelere(self):
if self.temps_ecoule() % DELAI_ACC == 0 and not self.deja_accelere:
self.deja_accelere = True
self.delai -= DELTA_DELAI
print(self.delai)
elif self.temps_ecoule() % DELAI_ACC != 0:
self.deja_accelere = False
def temps_ecoule(self):
return 0 if self.debut is None else self.time() - self.debut
def infos(self):
ecoule = self.temps_ecoule()
temps = self.font.render(f'{ecoule//60:02}:{ecoule%60:02}', True, COULEUR_TEXTE)
score = self.font.render(f'{self.score:03}', True, COULEUR_TEXTE)
self.ecran.blit(temps, POSITION_TEMPS)
self.ecran.blit(score, POSITION_SCORE)
def game_over(self):
big = pygame.font.SysFont(None, 36)
fin = big.render('GAME OVER', True, COULEUR_TEXTE)
self.ecran.blit(fin, CENTRE)
self.refresh_and_pause()
def effacer(self):
self.ecran.fill(COULEUR_FOND)
def refresh_and_pause(self):
pygame.display.flip()
pygame.time.delay(DELAI_MS)
def loop(self):
while not self.fini:
for event in pygame.event.get():
self.gerer_event(event)
if self.mode != MODE_STOP:
if self.delai > DELAI_MIN:
self.accelere()
self.effacer()
if self.mode == MODE_MOVE:
self.arene.anime()
self.arene.se_dessine()
self.infos()
self.refresh_and_pause()
pygame.quit()
def reset(self):
self.arene.reset()
snake_game = Jeu()
snake_game.start()
-
En réalite cela signifie que si on a beaucoup de textes à gérer le mieux est de créer un objet texte pour encapsuler les différentes étapes. ↩