Squash¶
Nous allons commencer notre apprentissage de la programmation et de Python, en construisant un jeu de squash (c’est-à-dire un « pong », mais à une seule raquette).
L’aire de jeu¶
Commençons par créer une fenêtre pour notre jeu, et plaçons-y notre balle.
Créez un fichier squash.py
vide. Vous pouvez facilement faire
cette opération grâce à gedit:
démarrez gedit avec
gedit squash.py &
ou bien démarrez gedit, et utilisez le menu
open
.
Introduisez alors le programme suivant dans l’éditeur:
import pygame
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
pygame.event.pump()
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.display.flip()
pygame.time.wait(5000)
pygame.display.quit()
pygame.quit()
Sauvegardez ce programme (menu save
) et exécutez-le en tapant
python3 squash.py
dans un terminal.
Avertissement
Il faut toujours bien se rappeler de sauver le fichier avant de l’exécuter. En effet, oublier cette sauvegarde résulte en l’exécution de la version précédente (la dernière sauvée) du programme.
Si tout s’est bien passé, une nouvelle fenêtre est apparue, pendant 5 secondes. Cette fenêtre montrait une balle jaune sur fond bleu.
S’il s’est passé autre chose, c’est que vous avez fait une erreur lorsque vous avez édité le programme. Regardez le message d’erreur que l’interpréteur Python a imprimé sur le terminal: celui-ci devrait vous aider à localiser le ou les problèmes dans votre code.
Le programme est assez court, mais il contient déjà plusieurs notions importantes. Explorons-les!
La première ligne import pygame
indique que votre programme
utilise des fonctionalités fournies par pygame
.
Variables¶
Notre programme utilise plusieurs variables. Une variable permet de mémoriser, et de réutiliser plus tard, une valeur ou un résultat. Chaque variable est caractérisée par un nom et une valeur. Une fois qu’il a été défini, le nom d’une variable ne peut plus changer. En revanche, la valeur d’une variable peut être changée par l’intermédiaire d’une affectation.
Par exemple, la ligne FENETRE_LARGEUR = 800
affecte la valeur
800
à la variable dont le nom est FENETRE_LARGEUR
. Comme c’est
la toute première fois que cette variable est utilisée dans le
programme, cette ligne définit (c’est-à-dire crée) la variable.
Dans nos programmes, nous utilisons la convention que les variables dont la valeur ne change pas (c’est-à-dire, les variables qui représentent des constantes) ont un nom en majuscules.
Dans notre programme, il y a 8 variables: BLEU_CLAIR
, JAUNE
,
FENETRE_LARGEUR
, FENETRE_HAUTEUR
, BALLE_RAYON
,
fenetre_taille
, fenetre
et balle_position
.
Tuples et couleurs¶
Les variables BLEU_CLAIR
et JAUNE
stockent chacune une valeur
qui représente une couleur. Dans Pygame, il y a plusieurs façons de
représenter une couleur, mais dans ce tutoriel, nous nous limiterons à
la représentation d’une couleur comme un mélange des trois couleurs
fondamentales: rouge, vert et bleu. La quantité de chaque couleur
fondamentale est représentée par un nombre entier de 0 à 255.
Par exemple, nous voyons que jaune est formé par la combinaison de quantités maximales de rouge et de vert, mais ne contient pas de bleu; alors que notre bleu clair ne contient pas de rouge mais contient un peu moins de vert que de bleu.
La notation (255, 255, 0)
indique que la valeur de JAUNE
est
un tuple, c’est-à-dire une séquence immuable de valeurs. « Séquence immuable »
veut dire que, non seulement les valeurs de la séquence ne peuvent
pas changer, mais aussi que la taille de la séquence est fixe.
La position d’une valeur dans la séquence définit son index, qui
peut être utilisé pour accéder à cette valeur. En informatique,
l’habitude fait que l’index du premier élément d’une séquence
possède souvent la valeur 0
.
Par exemple, la valeur de la composante rouge du bleu clair de notre
programme est BLEU_CLAIR[0]
et vaut 0
, la composante verte est
BLEU_CLAIR[1]
et vaut 191
et la composante bleue est
BLEU_CLAIR[2]
et vaut 200
.
Notez que la valeur de la variable fenetre_taille
est aussi un
tuple, dont le premier élément fenetre_taille[0]
vaut la valeur de
FENETRE_LARGEUR
(c’est-à-dire 800
), et dont le deuxième élément
fenetre_taille[1]
vaut FENETRE_HAUTEUR
(c’est-à-dire 600
).
Créer une fenêtre¶
La commande pygame.display.set_mode(fenetre_taille)
crée une
fenêtre de la taille spécifiée par le tuple passé entre
parenthèses. Le résultat de cette création de fenêtre est stocké dans
la variable fenetre
, qui représente donc la fenêtre de jeu.
La fenêtre représente l’aire de jeu. Celle-ci se compose de points (pixels) dans un plan dont le système de coordonnées est repésenté sur la figure.
Introduction aux listes¶
La variable balle_position
contient une liste, c’est-à-dire une séquence
mutable de valeurs. « Séquence mutable » signifie que, non seulement les
valeurs de la séquence peuvent changer, mais aussi que le nombre de
valeurs dans la séquence peut varier.
On peut aussi accéder aux valeurs d’une liste par indexation. Par
example, dans notre programme, balle_position[0]
vaut 10
.
Nous reviendrons plus longuement sur l’utilisation et la manipulation des listes plus tard.
Quelques commandes de Pygame¶
La commande pygame.init()
initialise Pygame, et doit toujours être
la première commande de Pygame exécutée par un programme.
La commande pygame.event.pump()
est nécessaire pour assurer le bon
fonctionnement du programme, mais son explication peut être ignorée.
fenetre.fill(BLEU_CLAIR)
colorie l’ensemble de notre fenêtre en
bleu clair.
La commande pygame.draw.circle(fenetre, JAUNE, balle_position,
BALLE_RAYON)
trace dans la fenêtre un cercle de couleur jaune et de
rayon BALLE_RAYON
, dont le centre est à la position
balle_position
.
La commande pygame.display.flip()
affiche le contenu de la
fenêtre à l’écran.
La commande pygame.time.wait(5000)
pause notre
programme pendant 5000 milisecondes (c’est-à-dire 5 secondes).
Finallement, les commandes pygame.display.quit()
et
pygame.quit()
permettent de quitter Pygame proprement.
Faire bouger la balle avec une boucle for
¶
Nous allons modifier notre programme précédent, de sorte à faire bouger la balle sur le terrain.
Pour ce faire, nous intégrons les lignes pygame.draw.circle(fenetre,
JAUNE, balle_position, BALLE_RAYON)
et pygame.display.flip()
dans une boucle for
:
for x in range(0, FENETRE_LARGEUR + 2*BALLE_RAYON, 5):
pygame.event.pump()
balle_position[0] = x
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.display.flip()
pygame.time.wait(20)
La ligne for x in range(0, FENETRE_LARGEUR + 2*BALLE_RAYON, 5):
définit une boucle, qui va itérer sur les éléments d’un ensemble ou
d’une séquence. A chaque itération, l’élément en question est
stocké dans la variable x
.
Les commandes Python qui sont exécutées à chaque itération de la boucle définissent le corps de la boucle, et sont indentées (c’est-à-dire décalées), par rapport à la ligne qui définit la boucle.
Notez que toutes les commandes du corps de la boucle doivent être indentées de la même façon (même décalage). Python est assez borné à ce sujet!
Une fois le dernier élément de l’ensemble ou de la séquence traité, le programme continue avec l’éxécution de la commande qui suit la boucle (c’est-à-dire la première commande qui suit le commande de définition de la boucle et de même indentation que celle-ci). Il est de bonne pratique de laisser une ligne blanche entre la dernière ligne du corps de la boucle et la première ligne suivant cette boucle, afin de rendre le programme plus lisible.
Range¶
Pour pouvoir comprendre ce que fait notre boucle for
, nous devons
d’abord nous tourner vers la commande range
. Elle prend 1, 2
ou 3 nombres en arguments, et génère une séquence de nombres comme
suit:
range(b)
génère la séquence de tous les nombres entiers de 0 à b-1 inclus.
range(a, b)
génère la séquence de tous les nombres entiers de a à b-1 inclus.
range(a, b, s)
génère la séquence des nombres entiers de a au plus grand nombre plus petit que b, par pas de s (c’est-à-dire a, a + s, a + 2s, etc.) [1]
Dans la boucle de notre programme, la variable x
va donc prendre,
successivement, des valeurs allant de 0
à
FENETRE_LARGEUR + 2*BALLE_RAYON
, par pas de 5.
Comme ces valeurs sont successivement affectées, à chaque itération, à
balle_position[0]
qui représente la position horizontale du centre
du cercle correspondant à la balle, la balle va se déplacer
horizontalement le long de l’écran.
En effet, les deux lignes suivantes, comme expliqué précédemment, dessinent et affichent la balle (à sa nouvelle position) à l’écran.
La commande figurant à la dernière ligne de la boucle pause le programme pendant 20 millisecondes: la balle bougera donc 50 fois par seconde (pour un mouvement « coulé »).
Notez aussi que nous pouvons modifier la position horizontale de la
balle, stockée dans balle_position
, car cette variable stocke les
coordonnées dans une liste (c’est-à-dire dans une séquence « mutable »).
Animations¶
Comme vous avez pu vous en rendre compte, notre programme fait bouger la balle, mais celle-ci laisse une empreinte sur l’écran.
Afin d’améliorer la lisibilité de notre code, nous pouvons d’abord
définir les deux constantes H = 0
et V = 1
, juste en dessous
de la ligne import pygame
(par exemple), qui vont représenter les
index des coordonnées respectivement horizontale et verticale de
notre balle. En effet, la coordonnée horizontale de la balle est stockée dans le
premier élément d’une liste, alors que la coordonnée verticale est
stockée dans le deuxième élément. N’oubliez pas que la
numérotation des indexes commence toujours à 0 en Python!
Nous pouvons dès lors animer notre balle en modifiant notre boucle
for
comme suit:
for x in range(0, FENETRE_LARGEUR + 2*BALLE_RAYON, 5):
pygame.event.pump()
fenetre.fill(BLEU_CLAIR)
balle_position[H] = x
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.display.flip()
pygame.time.wait(20)
Les seuls changements sont que:
Nous utilisons la constante
H
, à la place de0
, pour nous référer à la position horizontale de la balle.La commande
fenetre.fill(BLEU_CLAIR)
est ajoutée à la boucle, et donc l’écran est complètement « effacé » (c’est-à-dire, « repeint » en bleu clair) à chaque itération de la boucle.
Un mot sur l’efficacité¶
Notre programme anime maintenant correctement la balle, mais au prix de devoir « effacer l’écran » à chaque itération de notre animation. Cette opération implique de devoir écrire 800 * 600 = 480000 fois la valeur de la couleur bleu clair à l’écran. L’exécution d’autant d’affectations prend évidemment un certain temps, même pour un ordinateur moderne.
Une façon plus efficace serait de ne repeindre en bleu que la position précédente de la balle, plutôt que de repeindre tout l’écran. En effet, le nombre de points à repeindre est bien moindre pour la balle que pour l’écran.
Nous pouvons donc remplacer la ligne fenetre.fill(BLEU_CLAIR)
de
la boucle par la commande
pygame.draw.circle(fenetre, BLEU_CLAIR, balle_position,
BALLE_RAYON)
.
Cette approche fonctionne très bien dans notre cas, car pour l’instant, seule la balle est affichée à l’écran. Mais lorsque nous aurons d’autres éléments affichés à l’écran (comme un score, par exemple), « effacer la balle » s’avérera plus compliqué que de simplement repeindre celle-ci en bleu. En effet, la balle pourrait alors se trouver au dessus d’un élément qui n’est pas bleu!
Une bonne façon de traiter ce genre de cas est alors de représenter chaque élément de l’image à l’écran par le cadre carré de limitation (« bounding box » ) qui l’entoure. En calculant les cadres de limitation qui s’enchevêtrent, on peut aisément, et efficacement, déterminer les éléments de l’image qui doivent être repeints.
Cependant comme cette méthode requiert plus de code et est moins évidente à comprendre, nous nous limiterons, dans ce premier projet d’informatique, à simplement repeindre tout l’écran. En effet, la vitesse d’affichage obtenue est acceptable pour nos jeux simples.
La notion de vitesse¶
La position de la balle est essentiellement représentée par un vecteur (au sens géométrique du terme) représentant la position du centre du cercle par rapport à l’origine du système de coordonnées de la fenêtre.
En physique, une vitesse est aussi représentée par un vecteur. Nous
pouvons faire de même dans notre jeu en introduisant un variable
balle_vitesse
dont la valeur est une liste à deux éléments, le
premier représentant la composante de la vitesse dans le sens horizontal
(de la largeur) et le deuxième représentant la composante de la vitesse dans
le sens vertical (de la hauteur): balle_vitesse = [5, 5]
.
Une fois cette vitesse définie, nous pouvons alors très facilement calculer la nouvelle position de la balle, à chaque itération de notre animation, en ajoutant simplement la vitesse à la position précédente:
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
Ces deux lignes de code expriment simplement que le changement de position de la balle dans le sens horizontal (resp. vertical) dépend de sa vitesse horizontale (resp. verticale) [2].
Notez que puisque nous avons défini la vitesse comme ayant des composantes égales à 5, la balle se déplacera, à chaque itération, d’un tout petit peu plus de 7 points, à 45 degrés dans la direction sud-est.
A la pacman, avec if
¶
Si nous n’y prenons pas garde, une fois arrivée au bord inférieur de la fenêtre, notre balle disparaîtra. A ce stade, une fois que la balle a quitté l’écran, nous pouvons simplement la faire réapparaître au bord opposé:
if balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON:
balle_position[H] = -BALLE_RAYON
if balle_position[V] >= FENETRE_HAUTEUR + BALLE_RAYON:
balle_position[V] = -BALLE_RAYON
Le mot clé if
permet de choisir d’exécuter ou non certaines lignes
de code, une seule fois, selon qu’une condition est ou n’est pas
remplie.
Dans le premier if
ci-dessus, la condition est balle_position[H]
>= FENETRE_LARGEUR + BALLE_RAYON
(le :
indique la fin de
l’expression de la condition). Cette condition est vraie quand la
position horizontale (du centre) de la balle est plus grande ou égale à
la largeur de la fenêtre augmentée du rayon de la balle (c’est-à-dire, quand la
balle est complètement sortie du côté droit de la fenêtre). Dans ce
cas, les lignes de code “à l’intérieur du if” [3] sont exécutées. Si
la condition est fausse, alors ces lignes de codes ne sont pas
exécutées.
On dit aussi que ces lignes de codes sont dans la branche du if
Quelles sont ces lignes de code dans notre programme ? Et bien, ce
sont les lignes de codes qui sont indentées d’un niveau vers la droite
par rapport au if
. Dans le premier if
, il n’y a qu’une seule
ligne de code de cette forme, qui replace la balle juste à gauche de
la fenêtre, hors de la vue: balle_position[H] = -BALLE_RAYON
.
Dans tous les cas, que la condition soit vraie ou fausse, l’exécution
du programe continue ensuite avec la première ligne de code qui suit le
if
. Dans notre exemple ci-dessus, le deuxième if
est évalué.
Ce deuxième if
teste si la balle sort de la fenêtre par le côté
inférieur de celle-ci, et la fait réapparaître par le côté supérieur.
Pour que la balle se déplace, par exemple, 1000 fois, il nous suffit alors de
remplacer la boucle for
existante par une nouvelle boucle: for n in range(0, 1000):
Notre programme complet est maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
for n in range(0, 1000):
pygame.event.pump()
fenetre.fill(BLEU_CLAIR)
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON :
balle_position[H] = -BALLE_RAYON
if balle_position[V] >= FENETRE_HAUTEUR + BALLE_RAYON:
balle_position[V] = -BALLE_RAYON
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.display.flip()
pygame.time.wait(20)
pygame.time.wait(5000)
pygame.display.quit()
pygame.quit()
Executez ce programme.
Notez que la balle se déplace à une vitesse d’environ 7 points toutes les 20 milisecondes, ce qui équivaut à 350 points par seconde. Puisque la boucle exécute 1000 itérations de 20 milisecondes chacune, elle dure donc 20 secondes et le programme se termine 25 secondes après avoir débuté (car il y a une temporisation de 5 secondes après la boucle).
Notez aussi qu’un if
qui est dans le corps de la boucle for
doit être indenté par rapport à celui-ci. De même, les lignes de la
branche d’un if
doivent être indentées par
rapport à celui-ci (et donc « doublement » indentées par rapport au for
).
Exprimer des conditions¶
Les instructions de branchement (if
) utilisent des conditions.
Une condition est soit vraie (valeur True
), soit fausse (valeur
False
). Une telle expression est une expression booléenne et
True
et False
sont des valeurs booléennes.
Des expressions booléennes peuvent être construites à partir de divers opérateurs, dont les principaux sont repris dans la table ci-dessous:
Opérateur |
Signification |
---|---|
a == b |
vrai si a est égal à b |
a != b |
vrai si a est différent de b |
a > b |
vrai si a est plus grand que b |
a >= b |
vrai si a est plus grand ou égal à b |
a < b |
vrai si a est plus petit que b |
a <= b |
vrai si a est plus petit ou égal à b |
not a |
vrai si a est faux ; faux si a est vrai |
Notez que l’opérateur d’égalité est ==
et non =
, car =
est
utilisé pour affecter (stocker) une valeur dans une variable.
Les conditions reprises dans la table permettent d’exprimer des conditions “simples”. Nous pouvons exprimer des conditions composées plus complexes en combinant des expressions simples.
Pour exprimer qu’une condition composée est vraie, si au moins une
condition simple parmi plusieurs est vraie, on combine ces expressions
simples avec des or
(“ou” en anglais). Par exemple, si vous avez
trois variables a
, b
et c
, chacune avec une valeur numérique, la
condition composée suivante sera vraie si soit a
est plus grand que b
,
soit a
est plus grand que c
, soit les deux: a > b or a > c
L’utilisation d’une telle condition dans un if
, par exemple, se ferait comme suit:
if a > b or a > c:
...
N’oubliez pas le :
qui marque la fin de la condition.
Pour exprimer qu’une condition composée est vraie, seulement si toutes
les conditions simples qui la composent sont vraies, on combine ces
expressions simples avec des and
(“et” en anglais). Par exemple,
si vous avez trois variables a
, b
et c
, chacune avec une
valeur numérique, la condition composée suivante sera vraie si a
est le plus grand des trois nombres: a > b and a > c
L’utilisation d’une telle condition dans un if, par exemple, se ferait comme suit:
if a > b and a > c:
...
Exécution indéterminée avec while
¶
Jusqu’à présent, la boucle principale (boucle for
) de notre
programme a exécuté un nombre déterminé d’itérations, à la suite
desquelles le programme se termine.
De telles boucles ne sont pas appropriées pour un jeu, où, souvent, ce sont les actions du joueur qui déterminent la durée de la partie.
Nous pouvons construire une boucle dont l’exécution continue jusqu’à
l’invalidation d’une condition en utilisant une boucle while
. Par exemple:
fini = False
while not fini:
...
Dans cette construction, la variable fini
est intitialisée à la
valeur False
. La boucle while
est alors exécutée tant que sa
condition est vrai (notez, comme pour le if
, la présence du :
en fin de condition). Cette condition est évaluée avant le début de
chaque nouvelle itération et cette itération ne sera exécutée que si la
condition est vraie. Dans notre exemple, la condition est not fini
,
et sera vraie tant que la valeur de fini est False
. Pour que cette
boucle se termine, il faudra, evidemment, que la valeur de la variable
fini
soit changée en True
, par la logique de jeu, dans le
corps de la boucle while
.
Comme pour le for
et le if
, le corps d’une boucle while
correspond simplement au code indenté sous le while
(et, de nouveau,
l’indentation d’un même niveau doit être identique).
Et, tout comme pour le for
et le if
, une fois la boucle
while
terminée, l’exécution du programme se poursuit par
l’éxécution de la première instruction qui suit la boucle, c’est-à-dire la
première instruction qui suit le corps de la boucle. Cette instruction
doit être indentée de
la même façon que la ligne qui contient le while
lui-même.
Traiter les entrées du joueur¶
Vous avez peut-être remarqué que, dans tous nos programmes jusqu’à présent, la croix en haut, à droite de la fenêtre, n’a aucun effet lorsque vous cliquez dessus. C’est parce qu” un tel événement d’entrées/sorties est transmis à Pygame, mais que nous n’y réagissons pas dans notre programme.
En effet, Pygame gère une file d’événements dans laquelle il stocke
les événements qu’il reçoit. La commande pygame.event.get()
renvoie une liste de tous les événements ṕrésents dans la file
d’événements, et retire ces événements de la liste. En d’autres termes,
pygame.event.get()
renvoie une liste de tous les événements qui se
sont produits depuis son évocation précédente.
Nous pouvons donc passer ces événements en revue, un par un, à l’aide
d’une simple boucle for
:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
A chaque itération de la boucle for
, la variable evenement
contient un événement. Un événement possède toujours un type, qui
indique quel genre d’interaction avec le programme a été effectuée
par le joueur.
Dans notre exemple, pygame.QUIT
est le type de l’événement
qui est déclenché lorsque le joueur clique sur la « croix de fermeture »
de la fenêtre. Toujours dans notre exemple, lorsque l’événement
pygame.QUIT
est détecté, la valeur de la variable fini
est
mise à True
. Si cette boucle est placée dans la boucle while
décrite plus haut, nous voyons donc qu’à chaque itération du
while
, le programme testera si le joueur désire fermer la fenêtre
de jeu, et obtempérera si tel est le cas: en effet, si la valeur de
fini
est True
lorsque la condition du while
sera évaluée
la prochaine fois, cette condition (not fini
) sera fausse et la
boucle sera terminée.
Contrôler le nombre d’images par secondes¶
Jusqu’à présent, nous avons simplement demandé au programme de s’arrêter 20 millisecondes après avoir affiché chaque image de notre animation. Cependant, cette pause est indépendante du temps qui est effectivement nécessaire à la construction de l’image.
Pour avoir une animation plus régulière, il faudrait pouvoir tenir compte de ce temps de génération et d’affichage des images avant de décider le temps de pause exact, entre deux images.
Heureusement, Pygame fournit une telle fonctionalité, à travers une
facilité de mesure du temps, appelée pygame.time.Clock
:
temps = pygame.time.Clock()
stocke dans la variable temps
la
notion de temps, et en appelant la commande temps.tick(50)
après
avoir affiché chaque image (c’est-à-dire, à la fin de chaque itération de la
boucle while
), Pygame garantit que l’affichage se fera avec
maximum 50 images par secondes. Si la vitesse d’affichage
descend sous ce nombre d’images, alors le programme ne fera aucune
pause entre les images, de manière à rester le plus proche possible du
taux de 50 images par secondes.
Premier programme interactif¶
Nous sommes maintenant en mesure d’incorporer ces nouvelles notions à notre programme, adin de le rendre interactif: le programme tournera indéfiniment, jusqu’à ce que le joueur ferme la fenêtre.
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
fini = False
temps = pygame.time.Clock()
#--- Boucle principale
while not fini:
#--- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
#--- Logique du jeu
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON :
balle_position[H] = -BALLE_RAYON
if balle_position[V] >= FENETRE_HAUTEUR + BALLE_RAYON:
balle_position[V] = -BALLE_RAYON
#--- Dessiner l'écran
fenetre.fill(BLEU_CLAIR)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
#--- Afficher (rafraîchir) l'écran
pygame.display.flip()
#--- 50 images par seconde
temps.tick(50)
pygame.display.quit()
pygame.quit()
Remarquez que le programme contient des lignes dont le premier
caractère non « blanc » est #
. Ces lignes sont des commentaires, et
sont ignorées par l’interpréteur Python.
Notez que comme on traite la file d’évènements, le
pygame.event.pump()
n’est plus nécessaire. Et nous ne
l’utiliserons plus dans ce tutoriel.
Il est non seulement de bonne pratique de commenter votre code, mais aussi de choisir des commentaires judicieux qui expliquent clairement les intentions du programmeur.
Rebonds et if-else
¶
Nous allons maintenant faire rebondir notre balle sur les côtés du terrain de jeu. Une façon très simple de programmer ces rebonds est la suivante:
Si la balle frappe le bord supérieur ou inférieur de la fenêtre, on inverse le sens (signe) de la vitesse verticale. En effet, si la balle se deplace vers le haut et frappe le bord supérieur de la fenêtre, alors la balle doit, dès lors, se déplacer vers le bas. Un raisonnement similaire s’applique à une frappe sur le bord inférieur. Le sens de déplacement gauche-droite ou droite-gauche n’est pas affecté par une frappe sur un bord horizontal.
Si la balle frappe le bord gauche ou le bord droit de la fenêtre, on inverse le sens (signe) de la vitesse horizontale.
Une condition telle que « si la balle frappe le bord supérieur ou
inférieur de la fenêtre » est relativement simple à coder: en effet, en
se rappelant que la position de la balle balle_position
represente
la position du centre du cercle représentant cette balle, la balle doit
nécessairement toucher un bord horizontal de la fenêtre si:
balle_position[V] < BALLE_RAYON or balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR
est vraie.
De même:
balle_position[H] < BALLE_RAYON or balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR
sera vraie si la balle touche un des bords verticaux de la fenêtre.
Nous aurions donc:
if balle_position[V] < BALLE_RAYON or balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
balle_vitesse[V] = -balle_vitesse[V]
if balle_position[H] < BALLE_RAYON or balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
balle_vitesse[H] = - balle_vitesse[H]
Nous pourrions coder ces règles simples, et remplacer les deux if
que nous avons dans le programme (qui donnent l’effet « pacman » à la
balle) par ces deux-ci, mais le résultat ne serait pas
satisfaisant. En effet, rappellez-vous que notre balle « saute » d’une
certaine distance 50 fois par seconde. Son déplacement n’est donc pas
continu, même s’il est suffisemment rapide pour que nos yeux et notre
cerveau l’interprètent en tant que tel. Comme illustré sur la figure
ci-dessous, la balle pourrait alors « pénétrer » dans le mur, ce qui est
clairement un manque d’étiquette pour une balle de squash!
Pour corriger ce défaut, lorsque nous détectons que la balle touche un des murs, nous pouvons nous assurer de corriger la position de la balle de sorte qu’elle reste toujours entièrement dans la fenêtre. Si ce test est effectué avant que la balle ne soit tracée à l’écran, notre problème sera résolu.
Nous proposons ici une méthode très simple pour corriger la position de la balle en cas de « pénétration » dans un mur:
Si la balle touche le mur droit, nous la repositionnons horizontallement vers la gauche de sorte que la balle effleure le mur droit.
Si la balle touche le mur gauche, nous la repositionnons horizontallement vers la droite de sorte que la balle effleure le mur gauche.
Si la balle touche le mur du haut, nous la repositionnons verticallement vers le bas de sorte que la balle effleure le mur du haut.
Si la balle touche le mur du bas, nous la repositionnons verticallement vers le haut de sorte que la balle effleure le mur du bas.
Nous avons déjà les conditions (complexes) qui détectent si la balle touche soit un mur horizontal, soit un mur vertical, mais maintenant, comme les actions à prendre sont différentes en fonction du mur touché, nous allons simplement réécrire nos tests en « séparant » ces conditions.
Le code pour détecter et gérer les situations où la balle touche un mur devient donc:
if balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
balle_position[H] = FENETRE_LARGEUR - BALLE_RAYON
balle_vitesse[H] = -balle_vitesse[H]
else:
if balle_position[H] < BALLE_RAYON:
balle_position[H] = BALLE_RAYON
balle_vitesse[H] = -balle_vitesse[H]
if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
else:
if balle_position[V] < BALLE_RAYON:
balle_position[V] = BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
Jusqu’à présent, nous avions utilisé les constructions if
pour
décider d’exécuter du code conditionnellement (en d’autres termes, le code était
exécuté ou pas en fonction de la valeur d’une condition).
Nous rencontrons ici une autre construction, le if-else
, qui
garantit qu’un des choix d’une alternative, sera, quoiqu’il arrive, exécuté: si la condition du if
est vraie, alors c’est le code du if
qui est
exécuté, mais si la condition est fausse, alors c’est le code du
else
qui est exécuté à la place.
Notre programme est donc maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
fini = False
temps = pygame.time.Clock()
#--- Boucle principale
while not fini:
#--- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
#--- Logique du jeu
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
balle_position[H] = FENETRE_LARGEUR - BALLE_RAYON
balle_vitesse[H] = -balle_vitesse[H]
else:
if balle_position[H] < BALLE_RAYON:
balle_position[H] = BALLE_RAYON
balle_vitesse[H] = -balle_vitesse[H]
if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
else:
if balle_position[V] < BALLE_RAYON:
balle_position[V] = BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
#--- Dessiner l'écran
fenetre.fill(BLEU_CLAIR)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
#--- Afficher (rafraîchir) l'écran
pygame.display.flip()
#--- 50 images par seconde
temps.tick(50)
pygame.display.quit()
pygame.quit()
Notez cependant que notre méthode de « résolution » de collisions avec un mur donne de bons résultats visuellement, mais n’est cependant pas parfaite. En effet, dans certaines circonstances, notre balle peut subir un petit « glissement », comme illustré sur la figure ci-dessous:
Cependant, comme ces anomalies de rebond sont assez petites, elles ne sont pas très « dérangeantes », et nous conserverons donc notre méthode pour sa simplicité.
La raquette¶
Maintenant que nous avons une balle qui rebondit sur nos murs, il est temps de créer notre raquette.
Nous commençons par définir quelques constantes:
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_LARGEUR
et RAQUETTE_HAUTEUR
définissent les
dimensions (en points) du rectangle qui représente notre raquette.
RAQUETTE_ESPACE
définit la distance au bord inférieur de la
fenêtre à laquelle la raquette se trouve toujours. En effet, notre raquette va
se déplacer horizontalement dans le bas de la
fenêtre. RAQUETTE_DEPLACEMENT
définit le déplacement horizontal de
la raquette, à chaque commande du joueur.
Au début du jeu, nous voulons que la raquette soit placée à la bonne
hauteur, et au milieu de la fenêtre (horizontallement). En Pygame, la
commande pygame.draw.rect
trace un rectangle. Cette commande prend
comme paramètres, entre autres, la position du coin supérieur gauche du
rectangle. Nous utiliserons donc ce point supérieur gauche comme
position de notre raquette:
raquette_position = [FENETRE_LARGEUR//2 - RAQUETTE_LARGEUR//2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
Dans cette expression, l’opérateur « // » effectue une division entière. Si l’on avait utilisé « / », le résultat de la division aurait été représenté comme un nombre réel, ce qui est problématique pour certaines fonctions de Pygame qui nécessitent des coordonnées entières.
Une fois dans la boucle de jeu, nous affichons notre raquette par la commande:
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
Le premier paramètre de rect
est l’identifiant de la fenêtre où le
rectangle doit être affiché, le deuxième est la couleur de notre rectangle (une
séquence de trois éléments), et
le troisième est une séquence de deux éléments étant chacun une
séquence de deux éléments, donnant respectivement la position du coin supérieur
gauche du rectangle et ses dimensions.
Nous définissons, en passant, la couleur rouge: ROUGE = (255,
0, 0)
. Cette ligne de code doit, bien entendu, apparaître dans le
code avant son utilisation comme paramètre de pygame.draw.rect
.
Une fonction pour déplacer la raquette¶
Pour déplacer la raquette, il nous suffit de simplement changer sa position horizontale.
Mais, comme un déplacement vers la gauche et un déplacement vers la droite ne diffèrent que par le sens (signe) du déplacement, nous avons ici une opportunité de réutiliser le même code, plutôt que de le dupliquer.
Python supporte la notion de fonction, qui correspond simplement à une série de commandes auxquelles on donne un nom, et que l’on peut invoquer depuis d’autres endroits du programme, en lui passant des variables et/ou des valeurs (ces variables ou valeurs sont communement appelés des paramètres).
Une fonction est définie par le mot clé def
, suivi du nom de la
fonction, suivi des paramètres (entre parenthèses et séparés par des
virgules), suivi d’un :
. Les instructions qui la
constituent (son corps) sont indentés par rapport à sa définition.
Par exemple, en définissant les constantes:
VERS_DROITE = 1
VERS_GAUCHE = -1
une fonction qui déplace la raquette vers la gauche ou vers la droite peut se définir de la façon suivante:
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
L’opérateur a += b
est un raccourci pour a = a + b
, et notre fonction ajoutera ou soustraira RAQUETTE_DEPLACEMENT
de raquette_position[H]
, selon que sens
soit 1
(VERS_DROITE
) ou -1
(VERS_GAUCHE
).
Pour appeler la fonction, il suffit d’exécuter, par exemple, l’instruction
deplacement_raquette(VERS_DROITE)
.
Notre fonction permet cependant à la raquette de sortir de l’écran.
Comme la position de la raquette est représentée par les coordonnées
de son coin supérieur gauche, on peut aisément tester si la raquette
sort par la gauche de l’écran en testant si sa coordonnée horizontale
devient négative. De même, la coordonnée horizontale du côté droit de
la raquette est raquette_position[H] + RAQUETTE_LARGEUR
et la
raquette sort par le côté droit de l’écran si cette coordonnée devient
supérieure à la largeur de la fenêtre. Nous pouvons, dès lors,
facilement modifier notre fonction de manière à empècher la raquette
de sortir de l’écran:
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
if raquette_position[H] < 0:
raquette_position[H] = 0
elif raquette_position[H] + RAQUETTE_LARGEUR >= FENETRE_LARGEUR:
raquette_position[H] = FENETRE_LARGEUR - RAQUETTE_LARGEUR
La seule nouveauté dans ce code est l’utilisation du mot clé elif
qui est un raccourci pour:
else
if ...
Entrées clavier¶
Les entrées clavier sont envoyées à Pygame sous forme d’événements (voir plus haut).
Il ne nous reste plus qu’à traiter ces événements de manière à contôler la raquette.
Nous voulons que le joueur utilise les touche « flèche droite » et
« flèche gauche » pour contôler la raquette. Dans Pygame, ces touches
possèdent respectivement les codes pygame.K_RIGHT
et
pygame.K_LEFT
. Nous pouvons donc définir les constantes
suivantes:
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
et ensuite modifier notre boucle de traitement des événements comme suit:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
Dans cette boucle, nous cherchons, en plus de l’événement
pygame.QUIT
, un événement pygame.KEYDOWN
qui est envoyé chaque
fois qu’une touche du clavier est enfoncée. Si une touche est
enfoncée, on vérifie si cette touche correspond à une de nos touches de
contrôle. Si tel est le cas, nous appelons alors la fonction
deplace_raquette
avec le sens approprié.
Notre programe, utilisant le raccourci elif
partout est
maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
#--- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
if raquette_position[H] < 0:
raquette_position[H] = 0
elif raquette_position[H] + RAQUETTE_LARGEUR >= FENETRE_LARGEUR:
raquette_position[H] = FENETRE_LARGEUR - RAQUETTE_LARGEUR
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
raquette_position = [FENETRE_LARGEUR//2 - RAQUETTE_LARGEUR//2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
fini = False
temps = pygame.time.Clock()
#--- Boucle principale
while not fini:
#--- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
#--- Logique du jeu
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
balle_position[H] = FENETRE_LARGEUR - BALLE_RAYON
balle_vitesse[H] = -balle_vitesse[H]
elif balle_position[H] < BALLE_RAYON:
balle_position[H] = BALLE_RAYON
balle_vitesse[H] = -balle_vitesse[H]
if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
elif balle_position[V] < BALLE_RAYON:
balle_position[V] = BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
#--- Dessiner l'écran
fenetre.fill(BLEU_CLAIR)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
#--- Afficher (rafraîchir) l'écran
pygame.display.flip()
#--- 50 images par seconde
temps.tick(50)
pygame.display.quit()
pygame.quit()
Utiliser des fonctions pour un code plus lisible¶
Nous allons maintenant écrire du code pour rendre notre programme plus lisible, et donc plus facile à comprendre et à maintenir, plutôt que d’introduire de nouvelles fonctionalités.
Nous avons, à plusieurs endroits de notre programme, des tests qui détectent si la balle et la raquette sortent de l’écran. Par exemple:
if raquette_position[H] < 0:
raquette_position[H] = 0
elif raquette_position[H] + RAQUETTE_LARGEUR >= FENETRE_LARGEUR:
raquette_position[H] = FENETRE_LARGEUR - RAQUETTE_LARGEUR
et:
if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
elif balle_position[V] < BALLE_RAYON:
balle_position[V] = BALLE_RAYON
balle_vitesse[V] = -balle_vitesse[V]
sont de tels tests.
De plus, dans chacun de nos tests, si l’objet (raquette ou balle) sort de l’écran, sa position est corrigée de sorte que l’objet reste sur l’écran.
Les choses sont compliquées par le fait que la position de la raquette est indiquée par la position de son point supérieur gauche, alors que la position de la balle est indiquée par la position de son centre. Cette « disparité » vient de la manière dont les primitives graphiques utilisées pour représenter ces objets (rectangle et cercle dans Pygame) représentent leur position.
Nous pouvons écrire une fonction qui teste si « le point le plus à droite de l’objet » se trouve « à la droite » d’un point donné et corrige, le cas échéant, la position de l’objet de manière à ce que son point le plus à droite ne dépasse jamais le point donné. Une façon simple de représenter « le point le plus à droite d’un objet » est de partir de la position de l’objet, et de donner la distance horizontale du point le plus à droite, par rapport à la position de l’objet. Notre fonction peut dès lors s’écrire:
def test_touche_droite(objet, largeur_droite, point_droit):
if objet[H] + largeur_droite >= point_droit:
objet[H] = point_droit - largeur_droite
return True
else:
return False
En effet, étant donné un objet (réprésenté par sa position), la
position horizontale de l’objet est objet[H]
et la position
horizontale du point le plus à droite de l’objet est donc objet[H] +
largeur_droite
(où largeur_droite
est la distance horizontale
entre la position de l’objet et le point le plus à droite de
l’objet). La condition du if
teste donc si le point le
plus à droite de l’objet se trouve à la droite du point donné
(représenté pas sa position horizontale point_droit
). Si tel est
le cas, la position horizontale de l’objet est corrigée de sorte que
le point le plus à droite de l’objet coincide avec le point donné et
notre fonction retourne le booléen True
pour indiquer que l’objet
avait bien atteint (ou même dépassé) le point donné. Sinon, la
fonction retourne le booléen False
pour indiquer que l’objet est
toujours à la gauche du point donné.
Notez que lorsqu’une fonction retourne une valeur explicitement
(avec une instruction return
), la valeur retournée prend la place
de l’appel à la fonction dans le code d’appel. Ainsi, les fonctions
que nous venons d’écrire pourront simplement être utilisées comme
conditions (d’instructions if
) et la valeur retournée
selectionnera la branche correcte du if
.
Par un même raisonnement, nous pouvons écrire une fonction qui teste si un objet a dépassé un point donné vers la gauche:
def test_touche_gauche(objet, largeur_gauche, point_gauche):
if objet[H] - largeur_gauche <= point_gauche:
objet[H] = point_gauche + largeur_gauche
return True
else:
return False
En se rappelant que objet[V]
est la position verticale d’un objet, nous pouvons écrire des fonctions testant si un objet
est plus haut ou plus bas qu’un point donné:
def test_touche_haut(objet, hauteur_haut, point_haut):
if objet[V] - hauteur_haut <= point_haut:
objet[V] = point_haut + hauteur_haut
return True
else:
return False
def test_touche_bas(objet, hauteur_bas, point_bas):
if objet[V] + hauteur_bas >= point_bas:
objet[V] = point_bas - hauteur_bas
return True
else:
return False
Exploiter des similitudes dans notre code¶
En y regardant de plus près, on s’apperçoit que le code de la fonction
test_touche_droite
est similaire à celui de la fonction
test_touche_bas
: en effet, les deuxième et troisième arguments
sont utilisés exactement de la même façon (même s’ils ont des noms différents), dans les mêmes opérations.
La seule différence est que test_touche_droite
utilise la
coordonnée horizontale de l’objet (objet[H]
) alors que
test_touche_bas
utilise la coordonnée verticale (objet[V]
).
Comme les arguments d’une fonction sont des variables locales à cette
fonction, le choix de leur nom n’a aucune incidence sur la justesse du
code [4] et c’est donc bien leur position dans la liste d’arguments
qui est importante. Nous changeons donc les noms des deuxième et
troisième arguments de test_touche_droite
et test_touche_bas
pour qu’ils soient (respectivement) les mêmes dans ces deux fonctions
et si nous utilisions un quatrième argument pour sélectionner la
coordonnée choisie, les corps de ces deux fonctions seraient
exactement les mêmes! Dans une telle situation, il serait clairement
utile de les fusionner en une seule et même fonction:
def test_touche_db(objet, distance, point, direction):
if objet[direction] + distance >= point:
objet[direction] = point - distance
return True
else:
return False
et nous pouvons, dès lors, réécrire nos fonctions
test_touche_droite
et test_touche_gauche
comme de simples
« fonctions emballages » (« wrapper » en anglais) qui délèguent leur
travail à test_touche_db
:
def test_touche_droite(objet, largeur_droite, point_droit):
return test_touche_db(objet, largeur_droite, point_droit, H)
def test_touche_bas(objet, hauteur_bas, point_bas):
return test_touche_db(objet, hauteur_bas, point_bas, V)
Nous notons, au passage, qu’il est tout à fait possible, et même normal, qu’une fonction appelle une, ou plusieurs, autres fonctions. Vous verrez même qu’une fonction peut même s’appeler elle-même, mais dans ce cas il faut prendre des précautions particulières pour éviter de tomber dans un cycle d’appels infini.
Le même raisonnement s’applique aux fonctions test_touche_gauche
et test_touche_haut
, ce qui donne le code suivant:
def test_touche_gh(objet, distance, point, direction):
if objet[direction] - distance <= point:
objet[direction] = point + distance
return True
else:
return False
def test_touche_gauche(objet, largeur_gauche, point_gauche):
return test_touche_gh(objet, largeur_gauche, point_gauche, H)
def test_touche_haut(objet, hauteur_haut, point_haut):
return test_touche_gh(objet, hauteur_haut, point_haut, V)
Avec ces fonctions, nous pouvons maintenant aisément vérifier, par exemple, si la raquette sort du côté gauche de l’écran par:
test_touche_gauche(raquette_position, 0, 0)
En effet, comme la position de la raquette correspond à son point supérieur gauche, le point « le plus à gauche de la raquette » est à une distance égale à 0 de sa position (deuxième argument), alors que le bord gauche de l’écran est à la coordonnée horizontale 0.
Pour tester, par exemple, si la balle sort par la droite de l’écran:
test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR)
car le point « le plus à droite de la balle » est à une distance d’un
rayon moins un point du centre de la balle (et le centre est sa
position), alors que le bord droit de l’écran possède une une coordonnée
horizontale égale à FENETRE_LARGEUR - 1
(et que comparer
BALLE_RAYON - 1
et FENETRE_LARGEUR - 1
revient à comparer
BALLE_RAYON
et FENETRE_LARGEUR
).
En remplaçant tous nos tests de positionnement par des appels à nos fonctions de test, et en gardant à l’esprit que ces fonctions ont pour effet de bord de corriger la position des objets de sorte à ce qu’il n’y ait pas de dépassement, notre programme devient:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
#--- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, 0)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR)
def test_touche_db(objet, distance, point, direction):
if objet[direction] + distance >= point:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction):
if objet[direction] - distance <= point:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit):
return test_touche_db(objet, largeur_droite, point_droit, H)
def test_touche_gauche(objet, largeur_gauche, point_gauche):
return test_touche_gh(objet, largeur_gauche, point_gauche, H)
def test_touche_haut(objet, hauteur_haut, point_haut):
return test_touche_gh(objet, hauteur_haut, point_haut, V)
def test_touche_bas(objet, hauteur_bas, point_bas):
return test_touche_db(objet, hauteur_bas, point_bas, V)
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
raquette_position = [FENETRE_LARGEUR//2 - RAQUETTE_LARGEUR//2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
fini = False
temps = pygame.time.Clock()
#--- Boucle principale
while not fini:
#--- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
#--- Logique du jeu
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR):
balle_vitesse[H] = -balle_vitesse[H]
elif test_touche_gauche(balle_position, BALLE_RAYON, 0):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR):
balle_vitesse[V] = -balle_vitesse[V]
elif test_touche_haut(balle_position, BALLE_RAYON, 0):
balle_vitesse[V] = -balle_vitesse[V]
#--- Dessiner l'écran
fenetre.fill(BLEU_CLAIR)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
#--- Afficher (rafraîchir) l'écran
pygame.display.flip()
#--- 50 images par seconde
temps.tick(50)
pygame.display.quit()
pygame.quit()
Notez que juste après l’initialisation de Pygame, la ligne
pygame.key.set_repeat(200, 25)
a été ajoutée. Cette ligne permet
la répétition automatique des touches du clavier, avec le premier
argument donnant le délai avant la première répétition (en
milisecondes) et le deuxième argument donnant la période de
répétition, toujours en milisecondes. Donc, donc notre cas, il y aura
un délai de 200 ms avant la première répétition, et la touche
enfoncée sera alors répétée toutes les 25 ms.
Le nom des arguments a, par contre, une grande influence sur la lisibilité et la compréhension du code, et doit donc être choisi avec beaucoup de considération.
Arguments de fonction par défaut¶
Nos fonctions de test ont comme effet de bord de corriger la position de l’objet testé de manière à ce que celui-ci ne dépasse jamais la position de référence (position de test). C’est le comportement que nous désirons la plupart du temps dans notre programme.
Mais comment pourrions-nous contrôler d’avoir cet effet de bord ou pas?
Nous pourrions simplement passer un argument supplémentaire aux fonctions de test pour indiquer si nous voulons qu’elles effectuent l’effet de bord ou non. Mais vous avez certainement déjà tapé le programme de la section précédente, et ajouter un argument aux fonctions de test conduirait à devoir modifier tous les appels à ces fonctions, ce qui ferait beaucoup de changements!
Peut-on espérer ajouter un argument aux fonctions de test, sans devoir changer tous leurs appels?
La réponse est, fort heureusement, oui!
Il nous suffit de donner à cet argument supplémentaire une valeur par défaut. Une telle valeur est spécifiée en l’affectant à l’argument lorsque celui-ci est déclaré dans la liste des arguments de la fonctions. Dans
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
...
separe
possède True
comme valeur par défaut. En d’autres termes, si
un appel à test_touche_droite
spécifie explicitement quatre
arguments, separe
aura comme valeur la valeur de ce quatrième
argument; par contre, lorsqu’un appel à test_touche_droite
ne
spécifie explicitement que trois arguments, alors separe
prendra la
valeur True
, sa valeur par défaut.
Nous pouvons donc adapter de la façon suivante nos fonctions de test, sans devoir modifier leurs appels dans le reste du code:
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
Notez que les arguments par défaut doivent obligatoirement être les derniers de la liste des arguments.
Décomposition logique grâce aux fonctions¶
Nous pouvons maintenant rendre notre programme modulaire et exposer plus explicitement sa logique en décomposant la boucle principale en fonctions:
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, 0):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR) \
or test_touche_haut(balle_position, BALLE_RAYON, 0):
balle_vitesse[V] = -balle_vitesse[V]
def dessine_court():
fenetre.fill(BLEU_CLAIR)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
La seule chose nouvelle dans le code ci-dessus est la deuxième ligne
de la fonction traite_entree
. Une fonction peut lire la valeur
de n’importe quelle variable globale (en d’autres termes, d’une variable définie en
dehors de toute fonction). Par contre, pour qu’une fonction puisse
écrire le contenu d’une variable globale, elle doit explicitement
déclarer que c’est bien la variable globale qu’elle désire modifier,
en « déclarant » la variable avec le mot clé global
dans son
corps [5]. En effet, sans cette déclaration, une variable locale du même
nom serait créée localement, et la modification ne persisterait pas au
delà de la fin de la fonction.
ATTENTION Vous ne devez avoir qu’une seule boucle de traitement des évènements dans votre programme. En effet, cette boucle vide la file d’attente des évènements encore non traités, et avoir plusieurs de ces boucles, s’intéressant à des évènements différents, pourrait faire disparaitre des évènements qui ne seraient alors jamais traités.
Vous vous demandez peut-être pourquoi une fonction comme
deplace_raquette
peut modifier la position horizontale de
la raquette (raquette_position[H]
) sans devoir déclarer
raquette_position[H]
comme étant globale. La réponse est un
peu subtile, et les détails sortent du cadre de ce projet, mais
en gros, c’est parce que raquette_position
est une séquence
(dans ce cas précis, c’est une liste) et qu’une séquence est
représentée de manière interne par une addresse. Lorsqu’on
manipule la séquence, cette addresse est lue, même si cette
lecture est faite dans le but de modifier une valeur contenue
dans la séquence elle-même. Mais ne vous tracassez pas si cette
explication vous parait un peu « nébuleuse »: rapellez-vous
simplement que le contenu d’une séquence « globale » peut être
changé par une fonction sans déclaration global
.
Notez aussi que nous avons modifié des instructions de branchement en cascade, où les différentes branches exécutent les mêmes actions, telles que:
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR):
balle_vitesse[H] = -balle_vitesse[H]
elif test_touche_gauche(balle_position, BALLE_RAYON, 0):
balle_vitesse[H] = -balle_vitesse[H]
en instructions de branchement simples, complètement équivalentes, basées sur des conditions composées:
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, 0):
balle_vitesse[H] = -balle_vitesse[H]
Notre programme est maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = (0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, 0)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, 0):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR) \
or test_touche_haut(balle_position, BALLE_RAYON, 0):
balle_vitesse[V] = -balle_vitesse[V]
def dessine_court():
fenetre.fill(BLEU_CLAIR)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
fini = False
temps = pygame.time.Clock()
# --- Boucle principale
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
anime()
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
pygame.quit()
Embellir le terrain de jeu¶
Nous allons maintenant ajouter des murs blancs sur les côtés gauche, haut et droit de notre terrain, de manière à ce que les limites de notre terrain soient plus explicites.
Pour ce faire, nous définissons les constantes suivantes:
BLANC = (255, 255, 255)
MUR_EPAISSEUR = 10
Dans la fonction dessine_court
, nous traçons ces « murs » qui ne
sont, en fait, que des rectangles Pygame:
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
Rappelez-vous que le troisième argument de rect
représente d’une
part le point supérieur gauche du rectangle et d’autre part ses
dimensions en largeur et en hauteur. La première ligne ci-dessus trace
donc le mur gauche, la deuxième ligne trace celui du haut
de l’écran et la troisième trace celui de droite.
Il ne nous reste plus qu`à modifier les points de références dans (certains de) nos tests qui vérifient si la balle ou la raquette sont toujours bien complètement à l’écran, de manière à tenir compte de l’épaisseur de ces murs.
Par exemple, lorsque nous vérifions si la balle touchait le coté droit du terrain avec:
test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR)
il nous suffit de modifier le troisième argument en:
test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR)
Le programme est maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR) \
or test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[V] = -balle_vitesse[V]
def dessine_court():
fenetre.fill(BLEU_CLAIR)
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
fini = False
temps = pygame.time.Clock()
# --- Boucle principale
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
anime()
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
pygame.quit()
Collisions¶
Nous allons maintenant nous intéresser aux interactions entre la balle et la raquette.
Cette interaction va dépendre d’où la balle arrive sur la raquette. Par exemple, si la balle arrive du dessus et frappe le haut de la raquette, sa vitesse verticale va être inversée pour un rebond vers le haut. Par contre, si la balle arrive d’un côté et frappe un des côtés de la raquette, c’est la vitesse horizontale de la balle qui va être inversée.
Nous pouvons donc commencer par déterminer les positions relatives de la balle et de la raquette.
La raquette est un rectangle et détermine neuf zones dans le plan, relativement à elle-même:
Une façon simple de représenter ces zones est de numéroter trois zones horizontales et trois zones verticales (en gris sur la figure ci-dessous), relatives à la raquette, et d’ensuite numéroter les neuf zones définies par la raquette comme étant la somme de leur « coordonnées » horizontale et verticale:
En Python, cette approche se traduit par la définition des constantes suivantes:
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
Nous pouvons maintenant écrire des fonctions qui calculent la position relative d’un point par rapport à la raquette. Comme le point qui nous intéresse est le centre de la balle, nous considérons que le centre de la balle sera toujours le point d’intérêt. Commençons par écrire une fonction qui détermine la position relative de la balle horizontalement à la raquette, représentée par son rectangle:
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
L’argument passé à la fonction est un tuple de deux valeurs, chacune
étant un tuple de deux valeurs. Donc rect[0]
est la première
valeur de l’argument, et est donc un tuple. rect[0][H]
est la
première valeur de ce tuple, censée représenter la coordonnée
horizontale du point supérieur gauche du rectangle passé en argument.
Si la position horizontale de la balle est à gauche de ce point, alors
nous déclarons notre balle dans la zone GAUCHE
relative à la
raquette.
De même, si le centre de la balle se situe à droite du côté droit de
la raquette, dont la position horizontale est la position horizontale
du point supérieur gauche de la raquette, plus la largeur de la
raquette (rect[0][H] + rect[1][H]
), alors la balle est déclarée
dans la zone horizontale droite de la raquette.
Sinon, la balle est « centrée » au dessus de la raquette.
La même logique s’applique pour la position relative verticale de la balle et de la raquette:
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
Equipé de ces deux fonctions, nous pouvons aisément combiner leur résultat de manière à obtenir la position relative du centre de la balle et de la raquette:
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
Notez bien que nous aurions pu passer le point qui nous intéresse comme argument de ces fonctions, plutôt que de forcer l’utilisation du centre de la balle.
Détection de collisions cercle-rectangle¶
Une façon classique d’aborder la détection de collisions est de d’abord considérer uniquement les cadres de limitation de chaque objet, c’est-à-dire les plus petits rectangles qui englobent l’objet complètement. Dans notre cas, le cadre de limitation de la raquette, qui est déjà un rectangle, est la raquette elle-même, alors que le cadre de limitation de la balle, qui est un cercle, est un carré, centré sur le centre du cercle, dont le côté est égal au diamètre du cercle.
S’il n’y a aucun enchevêtrement entre ces cadres de limitation, alors il est certain que la balle et la raquette ne sont pas en collision (c’est-à-dire, ne se touchent pas):
Par contre, s’il y a bien enchevêtrement, alors il se peut qu’il y ait collision entre les deux objets:
Nous pouvons facilement créer un rectangle représentant le cadre de limitation de la balle:
BALLE_DIAM = 2 * BALLE_RAYON
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
et, étant donné deux rectangles, Pygame nous fournit un test de leur
enchevêtrement, sous la forme rect1.colliderect(rect2)
, où
rect1
et rect2
sont de type Pygame.Rect
et colliderect
retourne True
s’il y a enchevêtrement des deux rectangles, et
False
sinon.
Nous pouvons commencer à écrire une fonction qui détecte s’il y a collision entre la balle et la raquette, et agit ensuite en conséquence:
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
...
Dans cette fonction, rect
est le cadre de limitation de l’objet
avec lequel nous testons la collision de la balle. Ici, ce sera la
raquette.
La condition du if
ne sera True
que si les cadres de
limitation s’enchevêtrent, et donc si une collision est possible. C’est
cette possibilité que le programme doit ensuite investiguer.
S’il y a une possibilité de collision, cette collision est avérée si la balle est dans une des zones relatives au dessus, à gauche, à droite ou en dessous de la raquette (ce dernier cas en fait impossible dans notre jeu, mais nous en tenons compte pour être complet, et peut-être pouvoir réutiliser notre code dans d’autres circonstances).
Nous évaluons donc la position relative de la balle et de la raquette, et prenons les actions qui s’imposent dans les quatres cas discutés ci-dessus:
...
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
balle_vitesse[H] = -abs(balle_vitesse[H])
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
balle_vitesse[H] = abs(balle_vitesse[H])
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
balle_vitesse[V] = -balle_vitesse[V]
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
balle_vitesse[V] = -balle_vitesse[V]
...
La seule nouvelle chose dans ce code est l’utilisation de la fonction
abs
, fournie par Python, qui donne la valeur absolue d’un
nombre. Nous utilisons la valeur absolue de la vitesse horizontale
lorsque nous détectons une collision avec les côtés droit et gauche de
la raquette, car cette collision pourrait avoir été causée par un
déplacement de la raquette vers la balle, quelle que soit la direction
de nouvement de cette balle. Par exemple, la balle pourrait voyager de
gauche à droite, et la raquette pourrait bouger de gauche à
droite. Dans ce cas, inverser la direction de mouvement horizontal de
la balle ne donnerait pas le résultat voulu.
Notez aussi que nous réutilisons les fonctions de test précédemment développées, et que ces fonctions « séparent » les objets enchevêtrés (mais avec un léger glissement potentiel, comme décrit dans la section « Rebonds »).
Nous devons maintenant traiter les cas où la balle touche la raquette « par un coin ». Notez que ces cas sont, précisément, les cas où un enchevêtrement des cadres de limitation ne donnent pas nécessairement lieu à une collision réelle.
Dans tous ces cas, nous devons donc d’abord nous assurer qu’une collision réelle a lieu. Pour ce faire, il nous suffit de « mesurer » la distance entre le centre du cercle et le coin de la raquette: si cette distance est plus petite que le rayon du cercle, il y a, bel et bien, collision.
Nous écrivons donc d’abord une fonction qui, étant donné deux points, calcule leur distance. En fait, le calcul de distance entre deux points implique normalement le calcul d’une racine carrée qui est une opération pouvant être lente. Mais dans notre jeu, nous pouvons travailler avec les carrés des distances, ce qui est plus rapide:
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
Dans les cas où la balle touche un coin de la raquette, nous renvoyons la balle avec une direction de 45 degrés dans la zone relative de la balle. Notez que ceci est, de nouveau, une approximation, mais dont l’effet visuel est acceptable:
...
rayon2 = BALLE_RAYON * BALLE_RAYON
...
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
...
Les fonctions collision_coin_...
appelées dans ce code replacent la
balle selon un axe de 45 degrés par rapport au coin de la raquette, et
changent la vitesse de la balle de manière appropriée:
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
balle_vitesse[H] = vitesse_h
balle_vitesse[V] = vitesse_v
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin(rect[0], -delta, -delta, -abs(balle_vitesse[H]), -abs(balle_vitesse[V]))
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(balle_vitesse[H]),-abs(balle_vitesse[V]))
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(balle_vitesse[H]), abs(balle_vitesse[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(balle_vitesse[H]), abs(balle_vitesse[V]))
Dans les fonction ci-dessus, la valeur 0.707
est une approximation
de sin(math.pi / 4)
(en d’autres termes, la moitié de la racine carrée
de 2), de manière à éviter d’effectuer répétitivement ce calcul plus précis
mais potentiellement lent.
Il ne nous reste plus qu’un seul cas à traiter: le cas où le centre de la balle est « dans » la raquette. Pour simplifier, nous faisons ici l’hypothèse que l’amplitude de la vitesse de la balle ne dépasse jamais le rayon, et que donc, si le centre de la balle se retrouve dans la raquette, ce doit être parce que la raquette et la balle se sont déplacées l’une vers l’autre (et que la collision s’est donc faite par un coté de la raquette):
...
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la
# vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une
# vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
balle_vitesse[H] = -abs(balle_vitesse[H])
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
balle_vitesse[H] = abs(balle_vitesse[H])
Notre fonction de test de collision est donc:
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
balle_vitesse[H] = -abs(balle_vitesse[H])
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
balle_vitesse[H] = abs(balle_vitesse[H])
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
balle_vitesse[V] = -balle_vitesse[V]
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
balle_vitesse[V] = -balle_vitesse[V]
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
balle_vitesse[H] = -abs(balle_vitesse[H])
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
balle_vitesse[H] = abs(balle_vitesse[H])
Il ne nous reste plus, dans la fonction anime
, une
fois que la balle a bougé et que l’on a testé les collisions avec les
murs, qu’à ajouter un test de collision entre la balle et la raquette
et de verifier si la balle est sortie par le bas de l’écran. Si tel
est le cas, on termine simplement la partie:
def anime():
global fini
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[V] = -balle_vitesse[V]
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
Notre programme complet est maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
global fini
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[V] = -balle_vitesse[V]
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
def dessine_court():
fenetre.fill(BLEU_CLAIR)
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
balle_vitesse[H] = -abs(balle_vitesse[H])
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
balle_vitesse[H] = abs(balle_vitesse[H])
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
balle_vitesse[V] = -balle_vitesse[V]
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
balle_vitesse[V] = -balle_vitesse[V]
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
balle_vitesse[H] = -abs(balle_vitesse[H])
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
balle_vitesse[H] = abs(balle_vitesse[H])
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
balle_vitesse[H] = vitesse_h
balle_vitesse[V] = vitesse_v
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin(rect[0], -delta, -delta, -abs(balle_vitesse[H]), -abs(balle_vitesse[V]))
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(balle_vitesse[H]),-abs(balle_vitesse[V]))
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(balle_vitesse[H]), abs(balle_vitesse[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(balle_vitesse[H]), abs(balle_vitesse[V]))
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
fini = False
temps = pygame.time.Clock()
# --- Boucle principale
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
anime()
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Notez que comme fini
est une variable globale, la fonction
anime
doit la déclarer comme telle avant de pouvoir la modifier.
Aussi, notre programme ne gère pas les cas où la balle se fait « coincer » entre un mur et la raquette. Que feriez-vous dans ce cas?
Ajouter un score¶
Nous allons maintenant ajouter un score, qui augmentera d’un point chaque fois que le joueur frappera la balle avec la raquette.
Pour ce faire, nous initialisons à zéro une variable globale
score
. Cette variable sera incrémentée, dans test_collision
,
chaque fois que nous détectons une collision qui garde la balle en
jeu.
Pour pouvoir afficher le score à l’écran, nous devons définir la police de caractères à utiliser:
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)
où le premier argument est le nom de cette police de caractères, le deuxième est sa taille, et le troisième indique que nous voulons une version « en gras » de cette police.
Pour afficher le score, dans dessine_court
, il nous faut alors construire
le message à afficher:
marquoir = police.render(str(score), True, BLEU)
où le premier argument est une chaîne de caractères construite à
partir d’un nombre, le deuxième indique que nous souhaitons un
« lissage des caractères » pour faire plus joli, et le troisième est la
couleur du message (définit par BLEU = ( 0, 0, 255)
).
Une fois le message construit, nous donnons l’ordre de l’afficher avec:
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
Le deuxième argument de blit
représente le coin supérieur gauche
de l“« image » représentant le score.
Pour être certain que la balle passe « au dessus » du score, il faut
ajouter ces lignes à dessine_court
avant la ligne qui dessine la
balle.
Notre programme complet est maintenant:
import pygame
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
global fini
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[H] = -balle_vitesse[H]
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
balle_vitesse[V] = -balle_vitesse[V]
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
global score
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
balle_vitesse[H] = -abs(balle_vitesse[H])
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
balle_vitesse[H] = abs(balle_vitesse[H])
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
balle_vitesse[V] = -balle_vitesse[V]
score += 1
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
balle_vitesse[V] = -balle_vitesse[V]
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
score += 1
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
score += 1
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
balle_vitesse[H] = -abs(balle_vitesse[H])
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
balle_vitesse[H] = abs(balle_vitesse[H])
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
balle_vitesse[H] = vitesse_h
balle_vitesse[V] = vitesse_v
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin(rect[0], -delta, -delta, -abs(balle_vitesse[H]), -abs(balle_vitesse[V]))
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(balle_vitesse[H]),-abs(balle_vitesse[V]))
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(balle_vitesse[H]), abs(balle_vitesse[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(balle_vitesse[H]), abs(balle_vitesse[V]))
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
balle_vitesse = [5, 5]
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)
score = 0
fini = False
temps = pygame.time.Clock()
# --- Boucle principale
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
anime()
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Problème 1¶
Vous allez modifier le code du programme ci-dessus. Tout d’abord, ajoutez les noms et prénoms des membres présents de votre groupe en commentaires au début du code.
Ensuite, vous allez ajouter une brique, où vous voulez, dans l’aire de jeu. Pour simplifier, le bas de la brique doit être plus haut que le dessus de la raquette (pour éviter que la raquette et la brique ne se touchent). La balle doit, bien sûr, rebondir sur cette brique et le score doit augmenter chaque fois que la balle touche la brique.
Une fois votre programme terminé, connectez-vous sur la plateforme de soumission de montefiore, l’un d’entre-vous doit y créer un groupe pour le cours du PPI, et ses partenaires doivent rejoindre ce groupe. Ensuite l’un d’entre-vous doit envoyer votre soumission. Ce tutoriel explique comment soumettre votre problème sur la plateforme de soumission.
Avertissement
Ce problème doit être soumis sur la plateforme de soumission avant le 08/10/2024-23h59. Si la deadline n’est pas respectée, vous aurez 0.
Gérer une vitesse¶
Jusqu’à présent, dans notre code, la vitesse de la balle été stockée dans une séquence (par exemple, une liste) représentant le vecteur (géométrique) de la vitesse. Cette repésentation est tout à fait acceptable, mais nous pouvons augmenter la flexibilité de notre code en séparant les deux constituants d’une vitesse: la direction et l’amplitude.
La direction de la vitesse est un vecteur, et nous pouvons donc la stocker dans une liste. La seule contrainte que nous imposerons est que ce vecteur soit normalisé (c’est-à-dire de norme [6] unitaire). L’amplitude de la vitesse est alors un nombre. Une telle représentation permettra de changer aisément l’amplitude de la vitesse sans en changer la direction, et vice versa.
la norme d’un vecteur est sa « longueur » (ou « amplitude »).
Si la norme de la vitesse est stockée dans la variable
vitesse_amplitude
et que sa direction est stockée dans la liste
vitesse_direction
, alors la fonction suivante construit un vecteur
vitesse, comme utilisé jusqu’à présent:
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
Remarquez que cette fonction retourne un tuple, et non une liste. Tout
deux sont des séquences, et la lecture de leurs éléments se fait de la
même manière. Par contre, un tuple est immuable, alors qu’une liste
peut être modifiée. Dans notre cas, le choix du tuple est justifiable
par le fait que notre programme va manipuler explicitement l’amplitude
et la direction de la vitesse, et qu’un nouveau vecteur de vitesse
pourra être regénéré, après chaque modification, en appellant la
fonction vitesse
.
Notez aussi que les composantes de ce tuple sont arrondies à l’entier le plus proche. C’est parce que notre vitesse est utilisée pour calculer la position de la balle sur l’écran, et que les fonctions d’affichage requièrent des coordonnées entières.
Rappelez-vous que notre algorithme de détection de collisions fait l’hypothèse que la grandeur de la vitesse de la balle n’est jamais plus grande que le rayon de la balle. Nous pouvons facilement gérer cette condition en définissant, par exemple, une constante représentant la valeur initiale de l’amplitude de la vitesse, ainsi qu’une constante définissant l’amplitude maximale:
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
La vitesse de la balle peut alors être initialisée:
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
là où balle_vitesse = [5, 5]
apparaissait. math.sqrt
calcule
la racine carrée de son argument (le module math
doit être importé
au préalable par import math
), et la direction de la vitesse de
notre balle est bien un vecteur unitaire.
Nous fournissons aussi une fonction qui permet de changer une des
composantes de la direction de la vitesse (quelle que soit cette
composante) et d’automatiquement reflèter ce changement dans
balle_vitesse
:
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
Dans cette fonction, balle_vitesse
doit être déclaré global
,
car la fonction modifie balle_vitesse
en lui assignant directement
un nouveau tuple.
Notre programme ne modifie, pour l’instant, que la direction de la
vitesse de la balle, et nous pouvons donc utiliser notre nouvelle
fonction change_vitesse
partout où notre code affectait une valeur
directement à balle_vitesse
. Par exemple, balle_vitesse[H] = -balle_vitesse[H]
devient change_vitesse(H, -vitesse_direction[H])
.
De même, tous les appels à la fonction resoudre_collision_coin
prennent les composantes horizontale et verticale de la vitesse de
la balle comme quatrième et cinquième arguments, mais ces arguments ne
sont utilisés que pour des changement de direction. Nous devons donc
aussi remplacer balle_vitesse[H]
et balle_vitesse[V]
par
vitesse_direction[H]
et vitesse_direction[V]
dans ces arguments.
Accélérer la balle en fonction de la durée de jeu¶
Nous pouvons maintenant aisément implémenter une fonction qui permet d’accélérer la balle après un certain nombre de frappes:
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
La constante FACTEUR_AUGMENTATION_VITESSE
définit que l’amplitude
de la vitesse augmentera de 10% toutes les
FRAPPES_AUGMENTATION_VITESSE
(ici, toutes les 7 frappes).
Dans score % FRAPPES_AUGMENTATION_VITESSE
, nous rencontrons
l’opérateur %
(« modulo ») qui a pour valeur le reste de la division
entière de la première opérande par la seconde. Dans notre cas, la
condition score % FRAPPES_AUGMENTATION_VITESSE == 0
sera donc
True
lorsque le reste de la division entière de score
par
FRAPPES_AUGMENTATION_VITESSE
vaut 0, c’est-à-dire quand le score
est un multiple de FRAPPES_AUGMENTATION_VITESSE
(un multiple de 7
dans notre exemple). Lorsque nous assignons une nouvelle valeur à
vitesse_amplitude
, nous utilisons la fonction min
, fournie par
Python, qui retourne le plus petit de ses arguments, de sorte que
vitesse_amplitude
ne soit jamais plus grande que VITESSE_MAX
.
Il suffit alors d’appeler la fonction augmente_score
, depuis la
fonction test_collision
, à chaque fois qu’un rebond est détecté,
et ce, à la place du simple score += 1
.
Notez que comme test_collision
ne modifie plus directement
score
(qui est maintenant modifié par augmente_score
), la
ligne global score
peut être effacé de cette fonction.
Notre programme complet est maintenant:
import pygame
import math
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
global fini
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(H, -vitesse_direction[H])
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(V, -vitesse_direction[V])
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
change_vitesse(H, -abs(vitesse_direction[H]))
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
change_vitesse(H, abs(vitesse_direction[H]))
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
change_vitesse(V, -vitesse_direction[V])
augmente_score()
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
change_vitesse(V, -vitesse_direction[V])
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
augmente_score()
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
augmente_score()
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
change_vitesse(H, -abs(vitesse_direction[H]))
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
change_vitesse(H, abs(vitesse_direction[H]))
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
change_vitesse(H, vitesse_h)
change_vitesse(V, vitesse_v)
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin(rect[0], -delta, -delta, -abs(vitesse_direction[H]), -abs(vitesse_direction[V]))
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(vitesse_direction[H]),-abs(vitesse_direction[V]))
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)
score = 0
fini = False
temps = pygame.time.Clock()
# --- Boucle principale
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
anime()
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Ajouter des « effets » dans la frappe¶
Pour rendre notre jeu plus attrayant en donnant plus de contrôle au joueur, nous pouvons ajouter des « effets » lorsque la balle frappe la raquette.
Une technique très simple, mais efficace, pour implémenter ce genre de fonctionnalité, est de définir plusieurs zones sur la raquette, et de choisir l’angle de rebond de la balle en fonction de la zone sur laquelle la balle rebondit. Notez que dans ce cas, l’angle d’incidence de la balle est complètement ignoré.
Comme nous représentons maintenant la direction de la vitesse de la balle par un vecteur unitaire, nous allons écrire une fonction qui, étant donné un angle en degrés, génère le tuple représentant le vecteur unitaire dans cette direction. Les coordonnées d’un tel vecteur sont aisément calculées, grâce aux formules bien connues des triangles rectangles, et sont illustrées dans la figure suivante:
Notre fonction est donc:
def vecteur_unitaire(angle):
angle_radian = math.radians(angle)
return (math.cos(angle_radian), math.sin(angle_radian))
Nous souhaitons exprimer l’argument angle
en degrés, mais les
fonctions math.cos
et math.sin
, fournies par le module
math
de Python, doivent recevoir un angle exprimé en radians. La fonction
math.radians
effectue cette conversion de degrés en radians.
Considérons que nous souhaitons avoir quatre directions de rebond différentes sur notre raquette. Nous pouvons facilement générer un tuple contenant les vecteurs unitaires correspondants:
RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
Beaucoup de choses se passent dans cette ligne de code. D’abord, la
fonction tuple
, qui prend une séquence de valeurs comme seul argument,
construit un tuple dont les éléments sont ceux de la séquence donnée.
La commande vecteur_unitaire(a) for a in (-120, -100, -80, -60)
est du sucre syntaxique qui génère une séquence de valeurs et les
stocke dans un générateur. Sans entrer dans les détails, un
générateur est une séquence de valeurs qui ne peuvent être lues
qu’une seule fois.
Notez que les angles choisis sont négatifs car nous désirons que la balle rebondisse vers le haut.
Il ne nous reste plus qu’à diviser notre raquette en quatre zones, et à determiner laquelle de ces zones est touchée par la balle lorsque celle-ci frappe le haut de la raquette:
RAQUETTE_ZONES = 4
def zone_raquette(position_horizontale):
x_relatif = position_horizontale - raquette_position[H]
if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
return -1
return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)
Cette fonction, qui prend une position horizontale comme argument,
détermine si cette position est au dessus de la raquette. Si c’est le
cas, elle retourne le numéro de la zone, sinon elle retourne -1
.
Le numéro de zone servira d’index dans le tuple des vitesses de rebond,
afin de sélectionner la direction du rebond.
Il ne nous reste plus qu’à fixer la vitesse de rebond, à la
place de simplement « retourner » la vitesse de la balle verticalement,
lorsqu’une collision est détectée « au dessus de la raquette » (dans test_collision
):
zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
Notre programme complet est maintenant:
import pygame
import math
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
global fini
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(H, -vitesse_direction[H])
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(V, -vitesse_direction[V])
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
change_vitesse(H, -abs(vitesse_direction[H]))
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
change_vitesse(H, abs(vitesse_direction[H]))
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
augmente_score()
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
change_vitesse(V, -vitesse_direction[V])
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
augmente_score()
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
augmente_score()
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
change_vitesse(H, -abs(vitesse_direction[H]))
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
change_vitesse(H, abs(vitesse_direction[H]))
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
change_vitesse(H, vitesse_h)
change_vitesse(V, vitesse_v)
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin(rect[0], -delta, -delta, -abs(vitesse_direction[H]), -abs(vitesse_direction[V]))
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(vitesse_direction[H]),-abs(vitesse_direction[V]))
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
def vecteur_unitaire(angle):
angle_radian = math.radians(angle)
return (math.cos(angle_radian), math.sin(angle_radian))
def zone_raquette(position_horizontale):
x_relatif = position_horizontale - raquette_position[H]
if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
return -1
return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [10, 300]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)
score = 0
fini = False
temps = pygame.time.Clock()
# --- Boucle principale
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
anime()
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Un peu de suspens¶
Jusqu’à présent, notre jeu est tout à fait déterministe: pour une même séquence de touches frappées au clavier, aux mêmes moments, le jeu se comporte exactement de la même manière. Nous pouvons changer tout cela en décidant que lorsque la balle frappe un coin de la raquette, elle a autant de chance de « bien » rebondir (vers le haut) que de « mal » rebondir (vers le bas).
Nous avons donc maintenant quatre façons de rebondir sur les coins de notre raquette:
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))
et nous écrivons une fonction qui sélectionne une vitesse de rebond de manière aléatoire:
def vitesse_coin(cote):
if cote == VERS_DROITE:
v = RAQUETTE_REBOND_COIN[2:]
else:
v = RAQUETTE_REBOND_COIN[:2]
return random.choice(v)
Dans cette fonction, v
est une slice (c’est-à-dire une partie)
du tuple RAQUETTE_REBOND_COIN
. La notation [a:b]
utilisée
comme indice, sélectionne tous les éléments de la sous-séquence
commençant à l’indice a
et se terminant à l’indice b-1
(l’élément à l’indice b
est exclus). La notation [2:]
signifie
la sous-séquence commençant à l’indice 2
jusqu’à la fin de la
séquence, et [:2]
signifie la sous-séquence commençant au début de
la séquence, et se terminant à l’élément situé avant celui d’indice 2
.
Donc, si l’argument cote
vaut VERS_DROITE
, alors v
est un
tuple qui contient deux vecteurs unitaires, dont les angles sont
respectivement -45
et 45
degrés. Sinon, v
contient des
vecteurs dont les angles sont respectivement -135
et 135
.
La commande random.choice(v)
, du module random
qui doit être
importé, renvoie alors, au hasard, un élément de la séquence passée en
argument. Comme chacune des séquences v
construites contient un
« bon » et un « mauvais » rebond pour le coin correspondant de la
raquette, les rebonds sur les coins de la raquette sont maintenant
aléatoires.
La séquence de valeurs tirées aléatoirement est cependant
prédéterminée, c’est-à-dire qu’elle se répétera à l’identique à
chaque exécution du programme. Pour un choix plus aléatoire, dépendant
de l’heure (à la milliseconde près) à laquelle le programme
est lancé, il suffit d’exécuter random.seed()
une seule fois avant
le début de la boucle de jeu.
Il ne nous reste plus, dans les fonctions collision_coin_haut_gauche
et
collision_coin_haut_droit
, qu’à sélectionner une vitesse de rebond
avec vitesse_coin
:
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_GAUCHE)
resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_DROITE)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
Plusieurs vies¶
Nous introduisons maintenant plusieurs vies pour le joueur:
MAX_VIES = 3
Le principe de la modification du jeu est maintenant très simple: il
suffit de perdre une vie lorsque le joueur manque la balle, et d’englober la
boucle de jeu actuelle dans une autre boucle qui ne se termine que
lorsque le nombre de vies restantes est 0
. Bien évidemment, il
faut replacer la balle à l’écran au début de chaque nouvelle vie.
Nous désirons aussi instaurer une pause de deux secondes au début de
chaque vie, de manière à donner au joueur le temps de se
préparer: la balle ne doit pas bouger pendant deux
secondes, mais le joueur doit pouvoir déplacer sa raquette. Python,
dans le module time
(qui doit être importé), offre une fonction
time()
qui mesure le temps écoulé, en secondes, depuis un moment de
référence. Nous utilisons cette facilité pour mesurer nos pauses.
Finalement, nous affichons le nombre de vies restantes dans le mur supérieur du terrain de jeu.
Les changements requis sont sans difficultés:
import pygame
import math
import random
import time
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
MAX_VIES = 3
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini, vies_restantes
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
vies_restantes = 0
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE:
deplace_raquette(VERS_GAUCHE)
def anime():
global fini, vies_restantes
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(H, -vitesse_direction[H])
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(V, -vitesse_direction[V])
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
vies_restantes -= 1
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
for vies in range(vies_restantes - 1):
pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
change_vitesse(H, -abs(vitesse_direction[H]))
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
change_vitesse(H, abs(vitesse_direction[H]))
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
augmente_score()
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
change_vitesse(V, -vitesse_direction[V])
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
augmente_score()
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
augmente_score()
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
change_vitesse(H, -abs(vitesse_direction[H]))
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
change_vitesse(H, abs(vitesse_direction[H]))
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
def vitesse_coin(cote):
if cote == VERS_DROITE:
v = RAQUETTE_REBOND_COIN[2:]
else:
v = RAQUETTE_REBOND_COIN[:2]
return random.choice(v)
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
change_vitesse(H, vitesse_h)
change_vitesse(V, vitesse_v)
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_GAUCHE)
resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_DROITE)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
def vecteur_unitaire(angle):
angle_radian = math.radians(angle)
return (math.cos(angle_radian), math.sin(angle_radian))
def zone_raquette(position_horizontale):
x_relatif = position_horizontale - raquette_position[H]
if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
return -1
return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)
random.seed()
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)
score = 0
temps = pygame.time.Clock()
vies_restantes = MAX_VIES
# --- Boucle principale
while vies_restantes > 0:
balle_position[V] = FENETRE_HAUTEUR // 3
start = time.time()
auto_pause = True
fini = False
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
if not auto_pause:
anime()
else:
auto_pause = time.time() - start < 2.0
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Mettre le jeu sur pause¶
Implémenter une touche « pause » est très simple: il nous suffit de choisir une touche clavier (par exemple la touche « p ») pour changer la valeur logique (True/False) d’une variable booléenne globale représentant l’état de pause du jeu. Il nous suffit alors d’utiliser la valeur de cette variable globale pour inhiber les animations et les mouvements de la raquette (car nous ne voulons pas que le joueur utilise la facilité de pauser le jeu pour tricher):
import pygame
import math
import random
import time
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_PAUSE = pygame.K_p
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
MAX_VIES = 3
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini, vies_restantes, pause
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
vies_restantes = 0
elif evenement.type == pygame.KEYDOWN:
if evenement.key == TOUCHE_DROITE and not pause:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE and not pause:
deplace_raquette(VERS_GAUCHE)
elif evenement.key == TOUCHE_PAUSE:
pause = not pause
def anime():
global fini, vies_restantes
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(H, -vitesse_direction[H])
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(V, -vitesse_direction[V])
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
vies_restantes -= 1
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
for vies in range(vies_restantes - 1):
pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
change_vitesse(H, -abs(vitesse_direction[H]))
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
change_vitesse(H, abs(vitesse_direction[H]))
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
augmente_score()
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
change_vitesse(V, -vitesse_direction[V])
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
augmente_score()
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
augmente_score()
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
change_vitesse(H, -abs(vitesse_direction[H]))
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
change_vitesse(H, abs(vitesse_direction[H]))
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
def vitesse_coin(cote):
if cote == VERS_DROITE:
v = RAQUETTE_REBOND_COIN[2:]
else:
v = RAQUETTE_REBOND_COIN[:2]
return random.choice(v)
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
change_vitesse(H, vitesse_h)
change_vitesse(V, vitesse_v)
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_GAUCHE)
resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_DROITE)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
def vecteur_unitaire(angle):
angle_radian = math.radians(angle)
return (math.cos(angle_radian), math.sin(angle_radian))
def zone_raquette(position_horizontale):
x_relatif = position_horizontale - raquette_position[H]
if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
return -1
return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)
random.seed()
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)
score = 0
temps = pygame.time.Clock()
pause = False
vies_restantes = MAX_VIES
# --- Boucle principale
while vies_restantes > 0:
balle_position[V] = FENETRE_HAUTEUR // 3
start = time.time()
auto_pause = True
fini = False
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
if not auto_pause and not pause:
anime()
else:
auto_pause = time.time() - start < 2.0
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Ecran d’accueil¶
Pour avoir un écran d’accueil, vers lequel le jeu retourne après une partie, il suffit d’englober la boucle de jeu dans une autre boucle. Cette dernière ne se terminera que lorsque la touche “q” sera pressée alors que l’écran d’accueil est affiché.
Cette boucle initialise le jeu et doit traiter les entrées clavier, de sorte que le joueur puisse interagir avec notre écran d’accueil. Lorsque l’écran d’accueil est affiché, nous ralentissons donc la fréquence de rafraîchissement de l’écran, et de traitement d’entrées du joueur, à quatre fois par seconde (il ne sert strictement à rien d’aller plus vite), et nous jouons sur le fait que le jeu ne commencera que lorsque le joueur recevra des vies:
import pygame
import math
import random
import time
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_PAUSE = pygame.K_p
TOUCHE_QUITTE = pygame.K_q
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
MAX_VIES = 3
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini, vies_restantes, pause, joue
for evenement in pygame.event.get():
if delai:
return
if evenement.type == pygame.QUIT:
fini = True
vies_restantes = 0
joue = False
elif evenement.type == pygame.KEYDOWN:
if vies_restantes == 0: # Ecran intro
if evenement.key == TOUCHE_QUITTE:
joue = False
fini = True
else: # Toute touche (sauf Q) commence le jeu
vies_restantes = MAX_VIES
else:
if evenement.key == TOUCHE_DROITE and not pause:
deplace_raquette(VERS_DROITE)
elif evenement.key == TOUCHE_GAUCHE and not pause:
deplace_raquette(VERS_GAUCHE)
elif evenement.key == TOUCHE_PAUSE:
pause = not pause
elif evenement.key == TOUCHE_QUITTE:
fini = True
vies_restantes = 0
def anime():
global fini, vies_restantes
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(H, -vitesse_direction[H])
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(V, -vitesse_direction[V])
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
vies_restantes -= 1
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
for vies in range(vies_restantes - 1):
pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if vies_restantes == 0:
message = police.render("Au revoir...", True, JAUNE)
message_largeur, message_hauteur = police.size("Au revoir...",)
fenetre.blit(message, ((FENETRE_LARGEUR - message_largeur) // 2, 4 * FENETRE_HAUTEUR // 5))
if pause:
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 - 35, FENETRE_HAUTEUR//2 - 25), (30, 50)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 + 5, FENETRE_HAUTEUR//2 - 25), (30, 50)))
def dessine_intro():
fenetre.fill(BLEU_CLAIR)
titre = police_titre.render('Squash!', True, JAUNE)
titre_largeur, titre_hauteur = police_titre.size('Squash!')
fenetre.blit(titre, ((FENETRE_LARGEUR - titre_largeur) // 2, (FENETRE_HAUTEUR - titre_hauteur) // 4))
message1 = police.render("[Q]uitter", True, BLEU)
message1_largeur, message1_hauteur = police.size("[Q]quitter")
fenetre.blit(message1, ((FENETRE_LARGEUR - message1_largeur) // 2, 4 * FENETRE_HAUTEUR // 5))
message2 = police.render("N'importe quelle touche pour commencer...", True, BLEU)
message2_largeur, message2_hauteur = police.size("N'importe quelle touche pour commencer...")
fenetre.blit(message2, ((FENETRE_LARGEUR - message2_largeur) // 2, 4 * FENETRE_HAUTEUR // 5 + 1.2 * message1_hauteur))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
change_vitesse(H, -abs(vitesse_direction[H]))
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
change_vitesse(H, abs(vitesse_direction[H]))
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
augmente_score()
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
change_vitesse(V, -vitesse_direction[V])
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
augmente_score()
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
augmente_score()
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
change_vitesse(H, -abs(vitesse_direction[H]))
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
change_vitesse(H, abs(vitesse_direction[H]))
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
def vitesse_coin(cote):
if cote == VERS_DROITE:
v = RAQUETTE_REBOND_COIN[2:]
else:
v = RAQUETTE_REBOND_COIN[:2]
return random.choice(v)
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
change_vitesse(H, vitesse_h)
change_vitesse(V, vitesse_v)
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_GAUCHE)
resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_DROITE)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
def vecteur_unitaire(angle):
angle_radian = math.radians(angle)
return (math.cos(angle_radian), math.sin(angle_radian))
def zone_raquette(position_horizontale):
x_relatif = position_horizontale - raquette_position[H]
if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
return -1
return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)
random.seed()
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//20, True)
police_titre = pygame.font.SysFont('monospace', 80, True)
temps = pygame.time.Clock()
vies_restantes = 0
pause = False
joue = True
# --- Boucle principale
while joue:
delai = False
balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2,
FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
score = 0
traite_entrees()
dessine_intro()
pygame.display.flip()
temps.tick(4)
# --- Boucle partie
while vies_restantes > 0:
balle_position[V] = FENETRE_HAUTEUR // 3
start = time.time()
auto_pause = True
fini = False
delai = False
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
if not auto_pause and not pause and not delai:
anime()
if fini and vies_restantes == 0:
delai = True
fini = False
start = time.time()
else:
auto_pause = time.time() - start < 2.0
if delai and not auto_pause:
fini = True
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Interaction avec la souris¶
Pygame définit des événements d’entrée/sortie pour gérer la souris (voir la
documentation de Pygame). On peut détecter un clic par un événement de
type pygame.MOUSEBUTTONDOWN
. Tout événement evenement
de ce
type possède une variable evenement.button
qui indique quel bouton
a été cliqué et une variable evenement.pos
qui contient la
position du clic (dans une séquence de deux éléments).
Nous modifions donc notre boucle de traitement des événements d’entrée pour non seulement réagir à un clic du bouton gauche de la souris, mais aussi pour donner l’alternative au joueur de déplacer la raquette avec les touches “a” et “s”.
Lorsque le joueur clique sur la balle, son score est doublé. Par contre, si le joueur clique en dehors de la balle, alors son score est divisé de moitié:
import pygame
import math
import random
import time
H = 0
V = 1
BLEU_CLAIR = ( 0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
BLANC = (255, 255, 255)
BLEU = ( 0, 0, 255)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON
RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_DROITE2 = pygame.K_s
TOUCHE_GAUCHE2 = pygame.K_a
TOUCHE_PAUSE = pygame.K_p
TOUCHE_QUITTE = pygame.K_q
BOUTON_SOURIS_GAUCHE = 1
VERS_DROITE = 1
VERS_GAUCHE = -1
MUR_EPAISSEUR = 10
CENTRE = 0
GAUCHE = 1
DROITE = 2
DESSUS = 4
HAUT_GAUCHE = 5
HAUT_DROITE = 6
DESSOUS = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10
VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7
MAX_VIES = 3
# --- Définitions de fonctions
def deplace_raquette(sens):
raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)
def souris_cliquee(pos):
global score
if distance2(pos, balle_position) > BALLE_RAYON * BALLE_RAYON:
score = score // 2
else:
score = score * 2
def test_touche_db(objet, distance, point, direction, separe):
if objet[direction] + distance >= point:
if separe:
objet[direction] = point - distance
return True
else:
return False
def test_touche_gh(objet, distance, point, direction, separe):
if objet[direction] - distance <= point:
if separe:
objet[direction] = point + distance
return True
else:
return False
def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
return test_touche_db(objet, largeur_droite, point_droit, H, separe)
def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)
def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)
def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
return test_touche_db(objet, hauteur_bas, point_bas, V, separe)
def traite_entrees():
global fini, vies_restantes, pause, joue
for evenement in pygame.event.get():
if delai:
return
if evenement.type == pygame.QUIT:
fini = True
vies_restantes = 0
joue = False
elif evenement.type == pygame.KEYDOWN:
if vies_restantes == 0: # Ecran intro
if evenement.key == TOUCHE_QUITTE:
joue = False
fini = True
else: # Toute touche (sauf Q) commence le jeu
vies_restantes = MAX_VIES
else:
touche = evenement.key
if (touche == TOUCHE_DROITE or touche == TOUCHE_DROITE2) and not pause:
deplace_raquette(VERS_DROITE)
elif (touche == TOUCHE_GAUCHE or touche == TOUCHE_GAUCHE2) and not pause:
deplace_raquette(VERS_GAUCHE)
elif touche == TOUCHE_PAUSE:
pause = not pause
elif touche == TOUCHE_QUITTE:
fini = True
vies_restantes = 0
elif evenement.type == pygame.MOUSEBUTTONDOWN and evenement.button == BOUTON_SOURIS_GAUCHE and not (pause or auto_pause):
souris_cliquee(evenement.pos)
def anime():
global fini, vies_restantes
balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]
if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(H, -vitesse_direction[H])
if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
change_vitesse(V, -vitesse_direction[V])
test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
fini = True
vies_restantes -= 1
def dessine_court():
fenetre.fill(BLEU_CLAIR)
marquoir = police.render(str(score), True, BLEU)
fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
for vies in range(vies_restantes - 1):
pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
if vies_restantes == 0:
message = police.render("Au revoir...", True, JAUNE)
message_largeur, message_hauteur = police.size("Au revoir...",)
fenetre.blit(message, ((FENETRE_LARGEUR - message_largeur) // 2, 4 * FENETRE_HAUTEUR // 5))
if pause:
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 - 35, FENETRE_HAUTEUR//2 - 25), (30, 50)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 + 5, FENETRE_HAUTEUR//2 - 25), (30, 50)))
def dessine_intro():
fenetre.fill(BLEU_CLAIR)
titre = police_titre.render('Squash!', True, JAUNE)
titre_largeur, titre_hauteur = police_titre.size('Squash!')
fenetre.blit(titre, ((FENETRE_LARGEUR - titre_largeur) // 2, (FENETRE_HAUTEUR - titre_hauteur) // 4))
message1 = police.render("[Q]uitter", True, BLEU)
message1_largeur, message1_hauteur = police.size("[Q]quitter")
fenetre.blit(message1, ((FENETRE_LARGEUR - message1_largeur) // 2, 4 * FENETRE_HAUTEUR // 5))
message2 = police.render("N'importe quelle touche pour commencer...", True, BLEU)
message2_largeur, message2_hauteur = police.size("N'importe quelle touche pour commencer...")
fenetre.blit(message2, ((FENETRE_LARGEUR - message2_largeur) // 2, 4 * FENETRE_HAUTEUR // 5 + 1.2 * message1_hauteur))
def distance2(pt1, pt2):
delta_h = pt1[H] - pt2[H]
delta_v = pt1[V] - pt2[V]
return delta_h * delta_h + delta_v * delta_v
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
if balle_position[H] < rect[0][H]:
return GAUCHE
elif balle_position[H] > rect[0][H] + rect[1][H]:
return DROITE
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
if balle_position[V] < rect[0][V]:
return DESSUS
elif balle_position[V] > rect[0][V] + rect[1][V]:
return DESSOUS
else:
return CENTRE
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
return position_horizontale_rel(rect) + position_verticale_rel(rect)
# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
if ball_rect.colliderect(rect):
rayon2 = BALLE_RAYON * BALLE_RAYON
position = position_relative(rect)
if position == GAUCHE:
if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
change_vitesse(H, -abs(vitesse_direction[H]))
elif position == DROITE:
if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
change_vitesse(H, abs(vitesse_direction[H]))
elif position == DESSUS:
if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
augmente_score()
elif position == DESSOUS:
if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
change_vitesse(V, -vitesse_direction[V])
elif position == HAUT_GAUCHE:
if distance2(balle_position, rect[0]) <= rayon2:
collision_coin_haut_gauche(rect)
augmente_score()
elif position == HAUT_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
collision_coin_haut_droite(rect)
augmente_score()
elif position == DESSOUS_GAUCHE:
if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
collision_coin_bas_droite(rect)
else:
# Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
# Il faudra s'assurer que cette condition est toujours vraie.
# Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
delta_g = abs(balle_position[H] - rect[0][H])
delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
if delta_g < delta_d:
balle_position[H] = rect[0][H] - BALLE_RAYON
change_vitesse(H, -abs(vitesse_direction[H]))
else:
balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
change_vitesse(H, abs(vitesse_direction[H]))
def augmente_score():
global score, vitesse_amplitude, balle_vitesse
score += 1
if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
balle_vitesse = vitesse()
def vitesse_coin(cote):
if cote == VERS_DROITE:
v = RAQUETTE_REBOND_COIN[2:]
else:
v = RAQUETTE_REBOND_COIN[:2]
return random.choice(v)
def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
balle_position[H] = coin[H] + delta_h
balle_position[V] = coin[V] + delta_v
change_vitesse(H, vitesse_h)
change_vitesse(V, vitesse_v)
def collision_coin_haut_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_GAUCHE)
resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_haut_droite(rect):
delta = round(BALLE_RAYON * 0.707)
vitesse_rebond = vitesse_coin(VERS_DROITE)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])
def collision_coin_bas_gauche(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def collision_coin_bas_droite(rect):
delta = round(BALLE_RAYON * 0.707)
resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))
def change_vitesse(composante, val):
global balle_vitesse
vitesse_direction[composante] = val
balle_vitesse = vitesse()
def vitesse():
return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))
def vecteur_unitaire(angle):
angle_radian = math.radians(angle)
return (math.cos(angle_radian), math.sin(angle_radian))
def zone_raquette(position_horizontale):
x_relatif = position_horizontale - raquette_position[H]
if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
return -1
return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)
random.seed()
pygame.init()
pygame.key.set_repeat(200, 25)
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
fenetre.fill(BLEU_CLAIR)
RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))
police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//20, True)
police_titre = pygame.font.SysFont('monospace', 80, True)
temps = pygame.time.Clock()
vies_restantes = 0
pause = False
auto_pause = True
joue = True
# --- Boucle principale
while joue:
delai = False
balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()
raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2,
FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
score = 0
traite_entrees()
dessine_intro()
pygame.display.flip()
temps.tick(4)
# --- Boucle partie
while vies_restantes > 0:
balle_position[V] = FENETRE_HAUTEUR // 3
start = time.time()
auto_pause = True
fini = False
delai = False
while not fini:
# --- Traiter entrées joueur
traite_entrees()
# --- Logique du jeu
if not auto_pause and not pause and not delai:
anime()
if fini and vies_restantes == 0:
delai = True
fini = False
start = time.time()
else:
auto_pause = time.time() - start < 2.0
if delai and not auto_pause:
fini = True
# --- Dessiner l'écran
dessine_court()
# --- Afficher (rafraîchir) l'écran
pygame.display.flip()
# --- 50 images par seconde
temps.tick(50)
pygame.display.quit()
exit()
Problème 2¶
Vous allez ajouter un « power-up » à notre jeu: si, au cours d’une même vie, le joueur frappe la balle un certain nombre (prédéfini) de fois, la largeur de la raquette augmente de 50%. Ce « power-up » ne peut se produire qu’une seule fois par vie, et ne doit pas avoir d’effet d’une vie à l’autre.
Une fois votre programme terminé, connectez-vous sur la plateforme de soumission de montefiore, l’un d’entre-vous doit y créer un groupe pour le cours du PPI, et ses partenaires doivent rejoindre ce groupe. Ensuite l’un d’entre-vous doit envoyer votre soumission. Ce tutoriel explique comment soumettre votre problème sur la plateforme de soumission.
Avertissement
Ce problème doit être soumis sur la plateforme de soumission avant le 15/10/2024-23h59. Si la deadline n’est pas respectée, vous aurez 0.
Fonctionnalités supplémentaires¶
En utilisant les mécanismes de programmation que nous avons appris, vous avez maintenant tous les outils en main pour implémenter des fonctionnalités supplémentaires. Voici quelques suggestions:
Ajouter automatiquement une vie dès qu’un certain nombre de points sont atteints.
De temps à autre, faire apparaître brièvement des objets qui offrent un bonus (des points, une vie ou un « power-up ») s’ils sont touchés par la balle.
Placer des obstacles supplémentaires.
…
Amusez-vous!