Flappy¶
Nous poursuivons notre étude de Python et de Pygame avec un nouveau jeu, Flappy, qui utilise des images, plutôt que des primitives géométriques.
Notez déjà que dans cette partie du tutoriel, toutes les images
utilisées doivent être sauvegardées dans un sous-répertoire
images
, du répertoire où votre code se trouve: vous pouvez, d’ores
et déjà, créer ce sous-répertoire.
Notre héros, Flappy, est un oiseau:
Vous pouvez le télécharger, directement dans votre répertoire
images
, en faisant « clic-droit, enregistrer sous… » sur cette
image, et en selectionnant votre répertoire images
comme
répertoire cible pour la souvegarde.
Voici le code Python qui place Flappy dans un beau ciel bleu:
import pygame
BLEU_CIEL = (135, 206, 250)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
image = pygame.image.load('images/bird_wing_mid.png').convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
fini = False
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
fenetre.fill(BLEU_CIEL)
fenetre.blit(image, (50, 50))
pygame.display.flip()
temps.tick(10)
pygame.display.quit()
pygame.quit()
exit()
Les seules nouvelles commandes, dans ce programme, sont celles qui permettent de charger l’image dans Pygame:
image = pygame.image.load('images/bird_wing_mid.png').convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
pygame.image.load
charge l’image dont le nom de fichier est passé
en argument (dans notre exemple, le fichier bird_wing_mid.png
qui
se trouve dans le sous-répertoire images
du répertoire d’où l’on
exécute le programme).
Le résultat de cette commande est une Surface
Pygame, c’est-à-dire
un élément affichable à l’écran.
On applique ensuite la commande convert_alpha
à cette surface,
pour la rendre compatible avec les caractéristiques de l’écran (sauvées
dans la variable fenetre
).
La commande suivante, pygame.transform.scale
met l’image aux
dimensions voulues.
Vous devriez être familier avec le reste du programme. La seule chose
à se rappeler est que pour afficher Flappy à l’écran, il faut d’abord
« l’estampiller » sur la surface de l’écran avec fenetre.blit(image, (50, 50))
(le coin supérieur gauche de l’image de Flappy est
positionné aux coordonnées (50, 50)), et ensuite réafficher l’écran avec
pygame.display.flip
.
Notez que même si notre programme se contente d’afficher Flappy, sans rien faire d’autre, l’écran est quand même rafraîchi 10 fois par seconde.
Animations simples¶
Nous allons maintenant animer Flappy. Une animation est simplement une série d’images destinées à être affichées les unes après les autres, représentant différentes poses d’une « entité ».
Flappy est une telle entité. Une entité a plusieurs caractéristiques, telles qu’une position à l’écran, une ou plusieurs poses possibles, la pose prise actuellement, si l’entité est visible à l’écran ou pas, etc.
Python supporte la notion de dictionnaire, qui est une structure de données permettant de facilement regrouper, et gérer, plusieurs variables comme un ensemble cohérent. Un dictionnaire est un ensemble de paires clé-valeur, où la clé est un nom identifiant la valeur correspondante.
En Python, la syntaxe pour créer un dictionnaire contenant les caractéristiques d’une entité de jeu, comme décrite précédemment, est:
entite = {'visible': False,
'position': [0, 0],
'imageAffichee': None,
'poses': []
}
Dans ce dictionnaire, 'visible'
, 'position'
,
'imageAffichee'
et 'poses'
sont des clés [1] qui permettent
d’accéder aux valeurs correspondantes:
entite['visible']
est un booléen dont la valeur est False
. Cette valeur peut être
changée par entite['visible'] = True
.
De même, entite['position']
est la liste [0, 0]
,
entite['imageAffichee']
a la valeur None
(qui, en Python, représente une
absence de valeur) et entite['poses']
est une liste vide.
Notez au passage que le fait d’affecter une valeur à une clé
inexistante crée une nouvelle paire clé-valeur dans le
dictionnaire; par contre, essayer de lire une valeur correspondant à
une clé inexistante génère une erreur. Donc, évaluer entite['numero']
, qui
tente de lire la valeur associée à la clé 'numero'
(qui n’existe
pas dans notre dictionnaire) retourne une erreur. En revanche,
exécuter entite['numero'] = 1
crée la paire 'numero': 1
dans le
dictionnaire stocké dans la variable entite
, et une fois
cette ligne exécutée, entite['numero']
représente la valeur 1
.
Nous pouvons maintenant écrire des fonctions qui construisent et manipulent des dictionnaires représentant nos entités:
##### Definition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'imageAffichee':None,
'poses': []
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def position(entite):
return entite['position']
def ajoutePose(entite, image):
entite['poses'].append(image)
def prendsPose(entite, pose):
entite['imageAffichee'] = entite['poses'][pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
##### fin ENTITE #####
La fonction nouvelleEntite
crée et retourne un nouveau
dictionnaire (pour une nouvelle entité). Toutes les autres fonctions
prennent, comme premier argument, une variable contenant un
dictionnaire qui a été créé par nouvelleEntite
. De ce fait, ces
fonctions peuvent manipuler n’importe quelle entité (puisque chaque
entité est représentée par un dictionnaire différent).
La fonction ajoutePose
utilise la commande append
pour ajouter
une image (plus exactement, une surface Pygame) à la fin de la liste entite['poses']
(qui représente les différentes poses que peut prendre l’entité).
prendsPose
prend, comme second argument, l’indice de la pose
voulue dans la liste de poses.
Les autres fonctions ne devraient pas vous poser de problèmes.
Etant donnée une séquence d’entités entites
, la fonction
suivante affiche les entités visibles à l’écran ecran
:
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
dessine(objet, ecran)
Téléchargez les deux images suivantes dans votre sous-répertoire
images
:
Nous pouvons dès lors créer facilement une entité pour Flappy:
oiseau = nouvelleEntite()
for nom_fichier in ('bird_wing_up.png', 'bird_wing_mid.png', 'bird_wing_down.png', 'bird_wing_mid.png'):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, image)
Flappy, représenté par la variable oiseau
, possède donc quatres poses
possibles: la pose avec l’aile en position médiane a été insérée deux
fois dans la liste des poses, de manière à avoir une animation plus
régulière en cyclant sur cette liste.
Voici notre programme qui fait maintenant voler Flappy:
import pygame
BLEU_CIEL = (135, 206, 250)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'imageAffichee':None,
'poses': []
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def position(entite):
return entite['position']
def ajoutePose(entite, image):
entite['poses'].append(image)
def prendsPose(entite, pose):
entite['imageAffichee'] = entite['poses'][pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
##### Fin ENTITE #####
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
dessine(objet, ecran)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_fichier in ('bird_wing_up.png', 'bird_wing_mid.png', 'bird_wing_down.png', 'bird_wing_mid.png'):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, image)
pose = 3
nombrePoses = 4
place(oiseau, 50, 50)
scene = [oiseau]
attente = 0
fini = False
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
fenetre.fill(BLEU_CIEL)
attente += 1
if attente == 6:
pose = (pose + 1) % nombrePoses
prendsPose(oiseau, pose)
attente = 0
affiche(scene, fenetre)
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Dans la boucle principale, nous passons en cycle sur les poses de Flappy, et chaque pose est gardée pendant 6 images. Comme notre jeu opère avec 50 images par seconde, une seule image dure donc 20 millisecondes et Flappy change donc de pose toutes les 120 millisecondes, c’est-à-dire environ 8 fois par seconde.
Dictionnaires imbriqués¶
Flappy vole, mais le programmeur doit se rappeler dans quel ordre les poses ont été insérées dans la liste de poses. De plus, alors que Flappy n’a réellement que trois poses différentes (aile en position haute, médiane et basse), la liste des poses comporte un duplicata de la pose avec aile en position médiane, de sorte à simplifier l’animation.
Il serait cependant plus lisible, et plus facile pour le programmeur, de pouvoir référencer et manipuler des poses par un nom plutôt que par un numéro. Nous pouvons aisément implémenter cette fonctionnalité en remplaçant la liste de poses par un dictionnaire de poses dont les éléments associent un nom à une image:
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'imageAffichee':None,
'poses': {} # dictionnaire de nom:image
}
Et nous devons aussi modifier la fonction qui ajoute une pose à l’entité, pour permettre la spécification du nom de la pose, ainsi que l’ajout dans un dictionnaire plutôt que dans une liste:
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
Rappelez-vous qu’affecter une valeur à une clé qui n’existe pas dans le dictionnaire ajoute automatiquement cette paire clé-valeur dans le dictionnaire.
Nous changeons aussi le nom du deuxième argument de la fonction prendsPose
, pour reflèter qu’il s’agit maintenant d’un nom et plus
d’un indice:
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
Notez que ce dernier changement est purement cosmétique dans un but de documentation, car hormis le nom du deuxième argument, rien ne change dans cette fonction qui aurait très bien pu rester inchangée.
Dans le reste du programme, nous devons bien sûr adapter les appels à
la fonction ajoutePose
(qui prend maintenant 3 arguments au lieu
de 2).
Pour rendre les choses plus claires, Flappy n’a plus maintenant que
trois poses, correspondant aux noms de 'AILE_HAUTE'
,
'AILE_MILIEU'
et 'AILE_BASSE'
, et nous définissons
explicitement la séquence de 4 poses formant le cycle d’animation dans
le tuple sequence
:
import pygame
BLEU_CIEL = (135, 206, 250)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'imageAffichee':None,
'poses': {} # dictionnaire de nom:image
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
##### Fin ENTITE #####
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
dessine(objet, ecran)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
sequence = ('AILE_MILIEU', 'AILE_HAUTE', 'AILE_MILIEU', 'AILE_BASSE')
pose = 0
place(oiseau, 50, 50)
scene = [oiseau]
attente = 0
fini = False
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
fenetre.fill(BLEU_CIEL)
attente += 1
if attente == 6:
pose = (pose + 1) % len(sequence)
prendsPose(oiseau, sequence[pose])
attente = 0
affiche(scene, fenetre)
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
L’opération « modulo » (%
) obtient maintenant la valeur de sa seconde
opérande grâce à la commande len
qui retourne la taille de la séquence passée en argument. Le
deuxième argument de prendsPose
est maintenant le nom de la
pose à prendre.
Notez aussi que dans la boucle for
, étant donné que chaque élément de la
séquence sur laquelle on boucle est lui-même une paire de deux
éléments, on peut directement assigner chaque élément de cette paire à une
variable locale distincte [2].
Cette technique fonctionne avec n’importe quelle séquence de séquences.
Contrôler les animations finement¶
Flappy vole, mais l’animation est très fastidieuse. En effet, nous
avons une entité ayant plusieurs poses, l’animation est définie comme
un séquence de poses dans une variable globale (sequence
) et le
temps de chaque pose est géré dans la boucle principale (comme une
fonction d’un nombre d’images). Tout ceci est assez disparate!
Il serait bien plus simple de définir une animation comme une séquence de poses que l’entité prend pendant un certain temps chacune, d’associer ces animations à des entités, et de gérer ces animations depuis les entités elle-mêmes.
Nous commençons par définir un mouvement
, qui est une pose qui
dure un certain temps (exprimé en millisecondes):
##### Definition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### fin ENTITE #####
Une animation est alors simplement une suite de mouvements. Nous voudrions pouvoir répéter une animation en boucle ou bien la répéter un certain nombre de fois. Pour une animation « active » (en train d’animer une entité), il faut aussi non seulement savoir quel mouvement de l’animation est actif à un moment donné, mais aussi jusqu’à quand. Une animation peut donc être représentée par un dictionnaire:
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
La valeur associée à la clé 'repetition'
indique le nombre de fois
que l’animation sera répétée après la première animation
complète. Une valeur de 0
indique donc qu’une fois l’animation
complètée, celle-ci s’arrêtera. Par contre, une animation en boucle
(dont la valeur associée à 'boucle'
est True
) est répétée
indéfiniment (jusqu’à ce que l’animation soit explicitement arrêtée).
Nous pouvons maintenant écrire plusieurs fonctions utiles au contrôle et à la gestion des animations. Toutes ces fonctions prendront toujours, comme premier argument, l’animation sur laquelle elles agissent.
La fonction repete
est utilisée pour indiquer combien de fois une
animation doit être répétée (après la première itération complète sur
l’animation):
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
Notez qu’une animation qui doit être répétée un nombre fixe de fois, ne peut pas, en même temps, boucler indéfiniment.
Pour mettre une animation en mode « boucle indéfinie »:
def enBoucle(animation):
animation['boucle'] = True
Notez que lorsqu’une animation sera jouée en boucle, nous ignorerons
toute valeur de animation['repetition']
, et la fonction enBoucle
ne se
préoccupe donc pas de cette valeur du dictionnaire animation
.
Il n’y a pas de fonction particulière pour sortir une animation du
mode « en boucle »: tout appel à la fonction repete
résulte en
l’arrêt de la boucle courante (et repete(animation, 0)
stoppe
animation
à la fin de la boucle courante).
Il nous faut une fonction pour construire notre animation, mouvement par mouvement:
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
Rappelez-vous que animation['choregraphie']
est une liste de
mouvements, et ajouteMouvement
ajoute donc un mouvement à la fin
de cette liste.
Pour gérer l’animation, on doit pouvoir lui dire de commencer un mouvement donné de la chorégraphie:
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
La valeur animation['indexMouvement']
sert à se rappeler l’index du
mouvement en cours. La valeur animation['momentMouvementSuivant']
indique à quel moment l’animation doit terminer le mouvement en
cours. Cet instant est simplement calculé en ajoutant la durée du
mouvement qui commence, à l’instant présent. L’instant présent est
donné par pygame.time.get_ticks()
et est mesuré en millisecondes écoulées
depuis l’appel à pygame.init()
.
Commencer l’animation équivaut à commencer le mouvement 0:
def commence(animation):
commenceMouvement(animation, 0)
On arrête une animation en ne lui donnant aucun mouvement courant:
def arrete(animation):
animation['indexMouvement'] = None
On doit pouvoir demander à une animation, quel est le nom de la pose du mouvement actuel:
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
animation['choregraphie']
est une liste de mouvements, et l’indice
dans cette liste du mouvement courant est stocké dans
animation['indexMouvement']
:
animation['choregraphie'][animation['indexMouvement']]
récupère
donc le mouvement courant dans la liste et nous le passons à
nomMouvement
pour récupérer le nom de la pose correspondante. Bien
entendu, tout ceci n’est possible que si notre animation est « active »
et possède un mouvement courant. Dans le cas contraire, le mouvement courant
est None
.
Finalement, nous écrivons une fonction qui gère automatiquement l’état de l’animation:
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
Si cette fonction est appelée sur une animation qui n’est pas en cours, alors cette animation démarre (deux premières lignes). Sinon (en d’autres termes, si l’animation est en cours), si nous avons atteint, ou dépassé, l’échéance du changement de mouvement courant (troisième ligne), alors nous devons déterminer le mouvement suivant et le commencer. Dans ce cas, si le mouvement courant est le dernier de la chorégraphie de l’animation (lignes 4 à 12), plusieurs cas sont possibles: soit on anime « en boucle » et on recommence donc l’animation au début (ligne 5 et 6); soit on doit répéter l’animation (ligne 8 à 10); soit l’animation est terminée (ligne 12). Par contre, si nous ne somme pas à la fin de l’animation, alors on commence simplement le mouvement suivant (dernière ligne).
Grâce à la notion d’animation que nous venons de développer, nous pouvons maintenant modifier notre représentation des entités pour pouvoir leur associer plusieurs animations possibles, ainsi que, bien entendu, une animation « active »:
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None, #NOUVEAU
'animations':{} #NOUVEAU dictionnaire de nom:animation
}
Le dictionnaire imbriqué 'animations'
est un ensemble
d’animations, chacune identifiée par un nom, et le nom de l’animation
courante est associé à la clé 'animationActuelle'
(une valeur de
None
indique que l’entité n’est pas en animation).
Les fonctions existantes que nous avons pour manipuler des entités restent inchangées, mais nous pouvons ajouter des fonctions pour contrôler l’animation de nos entités:
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
commenceAnimation
indique, par son nom, quelle animation on désire
commencer, ainsi que combien de fois
on désire voir cette
animation. Par défaut, l’animation n’est effectuée qu’une seule
fois. Une valeur de 0
pour cet argument prend une signification
particulière: l’animation doit être effectuée en boucle. Notez que la sémantique de l’argument fois
est ici le nombre
de fois totale que l’on désire voir l’animation. Par contre, dans la
fonction repete
de gestion des animations, l’argument fois
représente le nombre de répétitions après l’animation initiale, et
c’est pourquoi cet argument doit être décrémenté d’une unité lorsqu’il
est passé à repete
.
Une entité en cours d’animation aura toujours une animation associée à
sa clé 'animationActuelle'
.
Pour arrêter l’animation en cours d’une entité, on arrête d’abord
l’animation explicitement (arrete(entite['animationActuelle'])
) et
on associe None
à la clé 'animationActuelle'
.
Il suffit alors de tester que la valeur associée à
'animationActuelle'
n’est pas None
pour déterminer si l’entité
est en animation.
Bien entendu, nous fournissons aussi une fonction pour ajouter une animation (et son nom) à une entité.
Nous devons alors modifier notre fonction affiche
pour qu’elle
assure l’animation des entités en cours d’animation. Notez qu’une
entité dont l’animation en cours se termine, gardera la pose du
dernier mouvement de l’animation:
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
Après avoir créé notre entité oiseau
et lui avoir ajouté ses trois
poses, nous pouvons maintenant aisément créer une animation de
'vol'
que nous lui ajoutons:
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 320))
ajouteAnimation(oiseau, 'vol', animation)
Notez que Flappy fait un petit vol plané entre deux coup d’ailes.
Nous voulons que Flappy vole continuellement:
commenceAnimation(oiseau, 'vol', 0)
Notre programme complet est maintenant:
import pygame
BLEU_CIEL = (135, 206, 250)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin ENTITE #####
##### Definition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
##### Fin ENTITE #####
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 320))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = [oiseau]
commenceAnimation(oiseau, 'vol', 0)
fini = False
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Notez bien la simplification de la boucle principale de jeu, puisque
c’est maintenant à partir de la fonction affiche
que toutes les
considérations de délais d’animation sont gérées.
Un peu de physique¶
Nous allons maintenant permettre à nos entités de se mouvoir. Pour ce faire, nous allons leur donner une vitesse et une accélération. Pour pouvoir contrôler aisément les mouvements, nous allons exprimer les vitesses en « pixels par seconde » ce qui nous donnera directement les déplacements à effectuer exprimés en nombres de pixels (points sur l’écran). Comme nous connaissons la taille de l’écran en pixels, cela nous permettra de contrôler directement la dynamique de notre jeu: par exemple, il sera très aisé de choisir la vitesse pour qu’une entité « traverse l’écran en 3 secondes ».
Avec des vitesses exprimées en pixels par seconde, l’accélération sera naturellement exprimée en pixels par seconde carrée (pixels par seconde par seconde, ce qui exprime bien une variation de vitesse par seconde).
Rappelez-vous que l’affichage de notre jeu se fait comme une série d’images mises à jour à intervalles réguliers. Il nous faudra donc pouvoir mesurer le temps depuis la dernière image pour pouvoir calculer les effets de l’accélération et de la vitesse sur une entité afin de déterminer sa nouvelle position dans la prochaine image.
Le dictionnaire pour une entité devient donc:
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0], # NOUVEAU
'acceleration': [0, 0], # NOUVEAU
'momentDeplacement': 0, # NOUVEAU
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
Nous ajoutons ensuite à nos entités des fonctions qui permettent de définir leur vitesse et leur accélération:
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
Dans ces fonctions, la vitesse et l’accélération sont exprimées par leurs composantes horizontale et verticale.
Etant donnés une accélération, une vitesse, une position et l’instant de la dernière mise à jour de l’entité, il est facile de calculer le déplacement de l’entité:
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
Ce code prend un temps, exprimé en millisecondes, comme deuxième
argument (rappelez-vous que pygame.time.get_ticks()
permet
d’obtenir ce temps). entite['momentDeplacement']
stocke le moment,
en millisecondes, du dernier déplacement effectué: dt
est donc une
mesure du temps qui s’est écoulé depuis le dernier déplacement,
exprimé en secondes. C’est exactement ce qu’il nous faut, car
l’accélération et la vitesse sont respectivement exprimées en pixels
par seconde carrée et en pixels par seconde.
La mise à jour de la vitesse fait l’hypothèse que l’accélération de
notre entité est restée constante depuis la dernière mise à jour. De
même, pour la mise à jour de la position, on considère que cette
accélération s’est appliquée instantanément à la vitesse au moment de
la mise à jour précédente, et que cette vitesse est alors restée
constante depuis. Ceci ne correspond bien sûr qu’à une approximation
de la réalité (où les accélérations s’appliquent de manière continue
et où les vitesses varient aussi de façon continue), et cette
approximation sera d’autant plus proche de la réalité que dt
sera
petit. Dans notre jeu, dt
vaut environ 20 milisecondes, ce qui donne un
très bon effet.
Finalement, la nouvelle mise-à-jour sera la mise-à-jour précédente
pour la suivante, et nous stockons donc l’instant de cette
mise-à-jour-ci dans entite['momentDeplacement']
.
Nous allons maintenant appliquer de la gravité (accélération vers le bas) à Flappy:
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixels/s^2
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
Nous écrivons une fonction pour déplacer toutes les entités de la scène (c’est-à-dire à l’écran) sous l’effet de l’accélération:
def miseAJour(scene):
maintenant = pygame.time.get_ticks()
for objet in scene:
deplace(objet, maintenant)
Et nous écrivons finalement une fonction qui traite les entrées de l’utilisateur, et qui va nous permettre de garder Flappy en vol en tapant, de manière répétée, sur n’importe quelle touche du clavier:
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
Notez qu’il faudra plus de 25 frappes de touche pour que Flappy traverse l’écran verticalement (car chaque mouvement vertical est d’un 25ème de la hauteur de l’écran, et que, entre deux enfoncements de touche, Flappy est soumis à la gravité). Notez aussi que la vitesse verticale de Flappy est nulle au moment où son ascension prend fin (ce qui est cohérent avec la réalité d’un objet en fin d’ascension).
Notre programme est maintenant:
import pygame
BLEU_CIEL = (135, 206, 250)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixels/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
##### Fin ENTITE #####
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
def miseAJour(scene):
maintenant = pygame.time.get_ticks()
for objet in scene:
deplace(objet, maintenant)
def traite_entrees():
global fini
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 320))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
scene = [oiseau]
commenceAnimation(oiseau, 'vol', 0)
fini = False
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
miseAJour(scene)
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Un écran d’attente¶
Flappy commence à tomber directement lorsque notre programme commence. Nous allons maintenant laisser le joueur choisir quand le jeu démarre.
Et, au passage, nous allons changer le comportement de Flappy: plutôt que de battre continuellement des ailes, il ne battra plus des ailes que lorsque le joueur enfoncera une touche.
Pour laisser le joueur choisir quand commence le jeu, nous
introduisons une variable booléenne enJeu
dont la valeur
sera True
si le jeu est commencé, et False
sinon.
Si le jeu n’est pas encore commencé, dans la boucle de jeu principale, nous affichons à l’écran un message invitant le joueur à commencer:
ORANGE = (255, 165, 0)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
enJeu = False
while not fini:
# --- Traiter entrées joueur
traite_entrees()
miseAJour(scene)
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
if not enJeu: # NOUVEAU
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur)//2, (FENETRE_HAUTEUR - messageHauteur)// 2)) # NOUVEAU
pygame.display.flip()
temps.tick(50)
Ces quelques lignes de code devraient à présent vous être familières.
Nous devons maintenant modifier la fonction traite_entrees
afin
d’y reflèter la différence de comportement, lorsqu’une touche est
frappée, selon que le jeu est commencé ou non:
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu: # NOUVEAU
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau): # NOUVEAU
commenceAnimation(oiseau, 'vol') # NOUVEAU
else: # NOUVEAU
enJeu = True # NOUVEAU
arreteAnimation(oiseau) # NOUVEAU
prendsPose(oiseau, 'AILE_MILIEU') # NOUVEAU
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1]) # NOUVEAU
De nouveau, il n’y a rien de bien compliqué dans nos modifications: si le jeu est commencé, nous réagissons à une frappe clavier en faisant monter Flappy, en commençant une animation de vol s’il n’y en a déjà pas une en cours (ayant été démarrée par une frappe clavier précédente). Si le jeu n’est pas encore commencé, la frappe clavier commence le jeu, et nous arrêtons l’animation de Flappy, lui donnons sa pose de repos (aile en position médiane), et le soumettons à la gravité.
Rappelez-vous que lorsqu’une animation se termine, l’entité garde la dernière pose, qui est la pose de repos.
Notre programme est maintenant:
import pygame
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixels/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
##### Fin ENTITE #####
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
def miseAJour(scene):
maintenant = pygame.time.get_ticks()
for objet in scene:
deplace(objet, maintenant)
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = [oiseau]
commenceAnimation(oiseau, 'vol', 0)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
miseAJour(scene)
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur)// 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Remarquez que nous avons changé la dynamique du vol de Flappy, en reduisant la longueur de la dernière pose de l’animation.
Problème 3¶
Flappy est quelque peu statique sur notre écran d’attente, car il a beau battre des ailes, il vole toujours au même niveau.
Dans ce problème, vous allez faire voler Flappy, sur cet écran d’attente, comme il vole pendant le jeu, à la différence que c’est votre code, et non le joueur, qui va générer les « impulsions » qui font monter Flappy.
Un des problèmes ici est de simuler d’une manière réaliste ces impulsions « comme si elles venaient d’un joueur humain », tout en gardant Flappy à l’écran la plupart du temps. En effet, un humain peut probablement générer entre 5 et 10 impulsions par seconde sur nos claviers, et la probabilité d’émettre une impulsion doit augmenter lorsque Flappy s’approche du bas de l’écran. Une impulsion doit obligatoirement être émise si Flappy commence à tomber hors de l’écran, et il ne doit pas y avoir d’impulsion du tout si Flappy commence à sortir par le haut de l’écran.
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 22/10/2024-23h59. Si la deadline n’est pas respectée, vous aurez 0.
Quelle distance Flappy parcourt-il?¶
Flappy se comporte de manière athlétique dans le ciel. Nous allons maintenant calculer la distance qu’il parcourt pendant ses prouesses de voltige.
Pour modéliser le déplacement horizontal de Flappy, nous lui associons une vitesse constante, mesurée en mètres par seconde:
VITESSE_HORIZONTALE = 4 # m/s
Notez que, contrairement à la vitesse verticale qui engendre un changement de position à l’écran, cette vitesse horizontale est purement abstraite. C’est pourquoi elle n’est pas incluse dans l’entité qui représente Flappy, et aussi pourquoi elle est exprimée en mètres par seconde au lieu de pixels par seconde.
La distance parcourue par Flappy va donc dépendre uniquement du temps qu’il passe à voler et sera le score réalisé par le joueur:
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
Comme nous calculons une distance à partir d’une vitesse, il faut nous
rappeler du moment où nous avons calculé le score pour la dernière
fois: cette valeur est stockée dans la clé 'derniereMiseAJour'
du
dictionnaire représentant le score.
Pour miseAJourScore
, nous utilisons la même approche que pour
déplacer une entité (via deplace
). Le laps de temps depuis la
dernière mise à jour est calculé (en secondes) et la distance totale
est augmentée de la distance parcourue depuis la dernière mise à jour.
Nous voulons pouvoir afficher le score continuellement à l’écran, et
la valeur du score sera donc non seulement mise à jour, mais aussi
lue, à chaque image (environ 50 fois par seconde). Cela vaut aussi dire que
la valeur du score est une valeur avec une virgule, ce qui n’est pas très joli
à montrer à l’écran. La fonction resultat
renvoie le score actuel
arrondi à une valeur entière.
Nous n’avons aucun contrôle entre le moment où le score est créé et le
moment où le joueur décide de commencer le jeu. La fonction
reinitialiser
remet le score à 0, mais surtout réinitialise le
moment où la distance commence à être mesurée.
Lorsque le jeu est mis à jour, dans miseAJour
, il faut maintenant
aussi mettre le score à jour, mais seulement si le jeu a
commencé. Cette fonction est donc maintenant:
def miseAJour(scene):
global fini
maintenant = pygame.time.get_ticks()
if enJeu: miseAJourScore(score, maintenant) # NOUVEAU
for objet in scene:
deplace(objet, maintenant)
Dans traite_entrees
, il faut aussi ajouter
reinitialiser(score)
lors de la première frappe d’une touche du
clavier.
Finalement, il nous suffit de créer un score avant d’entrer dans la boucle de jeu, et d’afficher ce score à chaque itération de celle-ci.
Notre programme est maintenant:
import pygame
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixels/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
def affiche(entites, ecran):
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
def miseAJour(scene):
maintenant = pygame.time.get_ticks()
if enJeu: miseAJourScore(score, maintenant)
for objet in scene:
deplace(objet, maintenant)
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = [oiseau]
commenceAnimation(oiseau, 'vol', 0)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
miseAJour(scene)
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Gérer la scène¶
Notre scène (c’est-à-dire notre aire de jeu) ne comporte actuellement qu’une seule entité, Flappy, et est représentée comme une liste stockée dans une variable globale. Cela convenait très bien tant que la composition de cette scène était statique, mais nous allons très bientôt avoir affaire à des entités qui entrent en scène, et quittent la scène, de manière plus dynamique.
Il sera donc utile d’avoir des fonctions qui facilitent la gestion de la scène, et il sera plus facile de pouvoir utiliser ces fonctions sans devoir se soucier de la représentation de la scène. Pour ce faire, nous utilisons la même technique que pour les mouvements, les animations, les entités ou le score: nous représentons la scène par un dictionnaire et passons le dictionnaire correspondant comme premier argument de toutes les fonctions opérant sur la scène:
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
nouvelleScene
crée une nouvelle scène, représentée par un
dictionnaire, n’ayant que la seule clé acteurs
à laquelle est
associée une liste (d’entités) [3], et retourne ce dictionnaire.
Nous aurions pu représenter la scène directement par une liste
retournée par nouvelleScene
. Mais la représentation par un
dictionnaire, même si ce dernier ne contient qu’une seule paire
clé-valeur, améliore la documentation du code et facilite, le
cas échéant, toute extension future.
ajouteEntite
prend une entité comme second argument et l’ajoute à
la scène. L’ordre dans lequel les entités sont ajoutées est
significatif: ce sera aussi l’ordre dans lequel les entités seront
affichées à l’écran.
enleveEntite
prend une entité comme second argument, et l’enlève
de la scène (la fonction remove
d’une liste prend une valeur et
retire la première instance de cette valeur trouvée dans la liste).
La fonction acteurs
retourne une copie de la liste des acteurs de la
scène. La copie se fait en passant la liste initiale à la fonction
list
fournie par Python (qui crée une liste à partir de la
séquence passée en argument).
miseAJour
calcule le déplacement, sous l’effet de la physique, des
entités de la scène. Cette fonction remplace celle écrite
précédemment, et nous en profitons pour la modifier: dans sa version
précédente, cette fonction déplaçait les entités de la scène et
appelait la fonction de mise à jour du score. Nous optons maintenant
pour une séparation claire de la mise à jour de la scène et de la mise
à jour du score, car ces deux activités sont logiquement indépendantes
l’une de l’autre. Nous ajoutons donc comme deuxième argument à
miseAJour
un moment (en millisecondes) plutôt que de laisser la
fonction calculer son propre temps (comme précédemment) et nous
enlevons la commande de mise à jour du score. (Il nous faudra remettre
cettre commande autre part: ce sera dans la boucle principale du jeu.)
Cette modification nous permet de nous assurer que toutes les
mises à jour (de nos entités, du score, etc) sont faites, pour une même
image, à un instant de référence commun.
Ensuite, nous avions la fonction affiche
qui prenait une liste
d’entités et les affichait à l’écran. Nous modifions cette fonction
pour qu’elle prenne une scène à la place de la liste. Notez que mis à
part le changement de nom du premier argument (ce changement n’a
qu’une valeur de documentation du code), la seule modification réelle
de cette fonction est l’ajout de la première ligne du corps de la
fonction, qui récupère la liste des acteurs de la scene.
Finalement, à la place de créer explicitement une liste contenant
l’entité oiseau
et de la stocker dans la variable scene
, nous
créons, et stockons dans scene
, une nouvelle scène (en appellant
nouvelleScene
) et y ajoutons Flappy avec ajouteEntite(scene,
oiseau)
.
Aussi, nous appelons miseAJour
et miseAJourScore
depuis la
boucle de jeu, pour que l’ensemble de nos mises à jour soient effectuées
pour chaque image:
import pygame
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixels/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
prendsPose(oiseau, 'AILE_MILIEU')
reinitialiser(score)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu: miseAJourScore(score, maintenant)
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Un temps nuageux¶
Flappy est pris dans un système atmosphérique comprenant plusieurs couloirs où le vent souffle avec des vitesses différentes. De plus, mais nous ne construirons cela que plus tard, ces couloirs sont pris en sandwich entre 2 énormes nuages.
Les nuages, qui vont occuper nos couloirs aériens, seront, bien entendu, représentés par des entités.
Avant d’implémenter notre ciel, nous anticipons sur deux besoins pour nos entités. Premièrement, une entité peut-être créée avant de commencer à se déplacer. Comme nous l’avons vu précédemment, une façon d’empêcher une entité de bouger est de lui affecter une accélération et une vitesse nulles, jusqu’à ce que l’on désire la mettre en mouvement. Lorsque nous n’avions que Flappy à gérer, cette stratégie était tout à fait appropriée, mais dans d’autres circonstances, il serait peut-être plus simple de spécifier les caractéristiques physiques d’une entité (vitesse, etc) au moment de sa création. Dans ce contexte, une autre façon de contrôler quand une entité commence à bouger est simplement de pouvoir régler explicitement le moment représentant la dernière mise à jour de l’entité (puisque ce moment est pris en compte pour le calcul du laps de temps écoulé depuis la dernière mise à jour):
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
Aussi, étant donné une entité, il est possible de récupérer son cadre de
limitation (voir figure ci-dessous) grâce à la commande Pygame
get_rect()
. Ce cadre de limitation, retourné par get_rect
est
un rectangle Pygame, dont les dimensions sont égales à celle de la
surface Pygame représentant l’entité, mais dont le coin supérieur
gauche est toujours (0, 0)
. Nous pouvons aisément construire un
nouveau rectangle Pygame représentant le cadre de limitation à la
position de l’entité, en utilisant la commande move
sur le
rectangle Pygame retourné par get_rect
, avec les coordonnées de
notre entité:
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
Nous implémentons maintenant notre ciel.
Nous définissons d’abord le nombres de couloirs aériens dans notre ciel:
COULOIRS_AERIENS = 5
Ce nombre de couloirs représente le nombre de couloirs « de jeu », mais comme nous voulons que Flappy soit « pris en sandwich » entre deux enormes nuages, nous ajoutons, en fait, deux « demi-couloirs », sur dessus et le dessous de l’écran, pour montrer le dessous du gros nuage supérieur et le dessus du gros nuage inférieur. L’image ci-dessous montre les couloirs de notre ciel:
Nous allons utiliser un dictionnaire contenant les caractéristiques utiles de notre ciel. Celles-ci sont:
le nombre de couloirs « entiers »;
la taille d’un couloir « entier »;
la coordonnée verticale du début de chaque couloir;
la vitesse horizontale du vent dans chaque couloir.
La fonction qui crée et retourne un ciel peut donc être écrite:
import random
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
Nous allons bientôt avoir besoin de nombres aléatoires, et comme notre
jeu n’aura qu’un seul ciel, la création de ce ciel est un bon endroit
pour initialiser notre générateur de nombres aléatoires, à l’aide de
la fonction random.seed
, de sorte que le jeu ne se comporte jamais
exactement deux fois de la même manière.
Nous calculons ensuite la taille d’un couloir entier: n’oubliez pas
que notre ciel a aussi 2 demis-couloirs. Comme nous voulons une taille
de couloir qui soit un nombre entier de points, nous utilisons une
division entière (//
). La taille d’un demi-couloir est clairement
la moitié de la taille d’un couloir entier.
La clé 'couloirs'
est associée à une liste dont chaque élément est la
coordonnée verticale du début du couloir correspondant (à l’indice de
l’élément dans la liste). La coordonnée verticale du demi-couloir du
dessus de l’écran est clairement 0
. L’opérateur +
, utilisée
entre deux listes, concatène ces listes, c’est-à-dire qu’une
nouvelle liste est créée, dont le début est la liste à la gauche du
+
et la fin est la liste à la droite de l’opérateur. La liste à la
droite du +
est une compréhension de liste, c’est-à-dire une
liste générée par un générateur (voir Squash - Ajouter des « effets »
dans la frappe). La compréhension de liste est une liste dont les
éléments sont ceux de la séquence générée par le générateur. Pour
comprendre cette compréhension de liste, rappelez-vous que
range(a)
génère une séquence contenant tous les entiers de 0
à
a-1
inclus.
La clé `'vents'`
est associée à une liste dont chaque élément est la
composante horizontale de la vitesse du vent dans le couloir
correspondant. L’opérateur *
appliqué à une liste, et dont
l’opérande de droite est un nombre entier, concatène la liste à
elle-même, autant de fois que spécifié par l’opérande de droite.
La signification des autres clés est évidente.
Nous aurons besoin de manipuler la vitesse du vent des différents couloirs:
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
La fonction changeVitesse
prend comme second argument l’indice
(numéro) du couloir pour lequel on souhaite modifier la vitesse. Le
troisième argument est considéré comme la « valeur absolue » de la
vitesse: nous voulons que le vent souffle toujours de droite à gauche,
ce qui correspond à une valeur négative de la composante horizontale
de la vitesse. changeVentHaut
et changeVentBas
sont des
fonctions « d’emballage » qui facilitent la modification de la vitesse
du vent dans nos deux demi-couloirs.
Les fonctions vitesseVent
, ventHaut
et ventBas
permettent
la lecture de la composante horizontale du vent du couloir
correspondant.
Nous avons ensuite des fonctions qui retournent la coordonnée verticale du début d’un couloir:
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
Pour faciliter la manipulation des couloirs « entiers », nous fournissons encore une fonction qui génère une séquence comprenant les indices de ces couloirs:
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
Nous devons, bien sûr, pouvoir générer des nuages dans notre ciel:
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
Le deuxième argument de nouveauNuage
est l’indice du couloir dans
lequel nous voulons le nuage, dont l’image est passée en troisième
argument.
Nous créons une entité pour notre nuage, et lui faisons prendre son
unique pose 'nuage'
. Pour donner un peu plus de dynamisme à notre
jeu, nous plaçons ensuite notre nouveau nuage verticalement, n’importe
où dans le couloir si le couloir est un couloir entier ou n’importe où
dans la moitié du couloir si le couloir est un demi-couloir (de sorte
que les nuages du dessus ou du dessous de l’écran soient toujours
partiellement visibles.). Pour ce faire, nous récupérons le cadre de
limitation de notre nuage, sachant que lorsqu’une des coordonnées de
ce rectangle [4] est modifiée, les autres coordonnées sont automatiquement
modifiées de manière à ce que le rectangle garde son aspect. En
d’autres termes, l’affectation d’une valeur à une coordonnée du
rectangle change la position de ce rectangle.
ces coordonnées sont rect.top
pour la coordonnée verticale
haute du rectangle, rect.bottom
pour sa coordonnée
verticale basse, rect.right
pour sa coordonnée horizontale
droite et rect.left
pour sa coordonnée gauche.
La fonction random.randint
retourne un entier choisi aléatoirement
et inclusivement entre ses deux arguments. Lorsque nous avons choisi
la position verticale du nuage, nous en profitons pour lui affecter
une vitesse (horizontale) égale à la vitesse du vent dans son
couloir. Finalement, nous plaçons le nuage juste « au delà » du côté
droit de l’écran, prêt à « entrer en scène ». La fonction se termine en
retournant l’entité nuage nouvellement créée.
De nouveau, pour faciliter la gestion de notre ciel, nous fournissons deux fonctions « d’emballage », pour la création de nuages dans les couloirs supérieur et inférieur de notre ciel:
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
Afin de tester notre ciel, téléchargez l’image de notre nuage dans
votre répertoire images
:
Nous chargons cette image dans notre jeu avec:
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
Nous créons un ciel et choisissons des vitesses aléatoires pour les couloirs:
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
Et nous créons trois nuages:
nuage = nouveauNuage(ciel, 3, IMAGE_NUAGE)
nuageh = nouveauNuageHaut(ciel, IMAGE_NUAGE)
nuageb = nouveauNuageBas(ciel, IMAGE_NUAGE)
que nous ajoutons à notre « scène » lorsque nous détectons la première frappe d’une touche (signifiant le début du jeu):
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
reveille(nuage) # NOUVEAU
ajouteEntite(scene, nuage) # NOUVEAU
reveille(nuageh) # NOUVEAU
ajouteEntite(scene, nuageh)# NOUVEAU
reveille(nuageb) # NOUVEAU
ajouteEntite(scene, nuageb)# NOUVEAU
Notre programme, mettant maintenant en scène trois nuages et Flappy, est:
import pygame
import random
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixel/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
COULOIRS_AERIENS = 5
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
reveille(nuage)
ajouteEntite(scene, nuage)
reveille(nuageh)
ajouteEntite(scene, nuageh)
reveille(nuageb)
ajouteEntite(scene, nuageb)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
nuage = nouveauNuage(ciel, 3, IMAGE_NUAGE)
nuageh = nouveauNuageHaut(ciel, IMAGE_NUAGE)
nuageb = nouveauNuageBas(ciel, IMAGE_NUAGE)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu: miseAJourScore(score, maintenant)
fenetre.fill(BLEU_CIEL)
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Gérer la scène automatiquement¶
Nous savons créer des nuages et les ajouter à notre scène, mais une fois en scène, les entités ne la quittent jamais. On remarque cependant qu’une fois qu’ils ont quitté l’écran par la gauche, les nuages ne reviendront jamais en scène. Ces nuages peuvent dès lors être détruits:
def enScene():
for acteur in acteurs(scene):
if acteur != oiseau and rectangle(acteur).right < 0:
enleveEntite(scene, acteur)
Il nous suffit alors d’appeler enScene
dans notre boucle de jeu,
pour garantir que les entités dans notre scène sont celles qui sont à
l’écran, ou sur le point d’entrer à l’écran.
Générer des nuages aléatoirement¶
Nous allons maintenant générer des nuages de manière aléatoire. Pour ce faire, nous avons besoin de deux processus aléatoires: un dans le temps, qui génère des instants de création de nuages, et l’autre « dans l’espace », qui génère le couloir aérien dans lequel un nouveau nuage apparaît.
Pour supporter la création aléatoire de moments dans le temps, nous pouvons créer un dictionnaire et des fonctions qui le manipulent, de sorte à générer des intervalles de temps dont la valeur est comprise entre un minimum et un maximum donné:
INTERVALLE_NUAGES = 1000 # ms
##### Définition MOMENTALEATOIRE #####
def nouveauMomentAleatoire(intervalle):
return {
'momentSuivant': 0,
'max': intervalle,
'min': intervalle // 2
}
def suivant(momentAleatoire, maintenant):
momentAleatoire['momentSuivant'] = maintenant + random.randint(momentAleatoire['min'], momentAleatoire['max'])
def estExpire(momentAleatoire, maintenant):
return momentAleatoire['momentSuivant'] <= maintenant
##### Fin MOMENTALEATOIRE #####
Les moments aléatoires générés sont mesurés en millisecondes. La
fonction suivant
génère un moment qui est, en moyenne, trois quarts
d”intervalle
millisecondes dans le futur.
estExpire
indique simplement si l’on a atteint, ou dépassé,
l’instant choisi par l’appel précédent à suivant
.
Pour générer des nuages dans notre ciel, nous devons d’abord nous rappeler que pour les couloirs supérieur et inférieur de l’écran, nous voulons une couverture dense, car ces couloirs ne sont que la partie visible de deux énormes nuages. Pour obtenir une couverture dense, sans éclaircie, il faut s’assurer que l’intervalle maximum d’apparition entre deux nuages dans chacun de ces couloirs est plus petit ou égal au temps qu’il faut à un nuage pour se déplacer de sa largeur. Ce temps est évidemment la largeur du nuage divisée par la vitesse du vent dans le couloir.
Pour les autres couloirs, nous nous contenterons de générer des nuages avec un intervalle maximum, et nous choisirons un couloir (entier) de manière aléatoire pour chacun de ceux-ci:
##### Définition BOUILLOIRE #####
def nouvelleBouilloire(intervalle):
return {
'haut': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventHaut(ciel))),
'bas': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventBas(ciel))),
'autre': nouveauMomentAleatoire(intervalle),
'zones': {'haut', 'bas', 'autre'}
}
def faitNuage(bouilloire, maintenant):
for zone in bouilloire['zones']:
moment = bouilloire[zone]
if estExpire(moment, maintenant):
suivant(moment, maintenant)
if zone == 'haut':
nuage = nouveauNuageHaut(ciel, IMAGE_NUAGE)
elif zone == 'bas':
nuage = nouveauNuageBas(ciel, IMAGE_NUAGE)
else:
couloir = random.randint(1, COULOIRS_AERIENS)
nuage = nouveauNuage(ciel, couloir, IMAGE_NUAGE)
reveille(nuage)
ajouteEntite(scene, nuage)
##### Fin BOUILLOIRE #####
Notre bouilloire a ainsi trois générateur de moments aléatoires: le
générateur 'haut'
est utilisé pour gérer le couloir supérieur,
'bas'
pour le couloir inférieur et 'autre'
pour les autres
couloirs. Lorsque le moment est venu de générer un nuage, nous
sélectionnons le moment de génération du nuage suivant (dans la même
zone du ciel), et ensuite le nuage est créé et ajouté à la
scène. Notez que notre bouilloire utilise les facilités fournies par
le ciel pour créer les nouveaux nuages.
Il ne nous reste plus qu’à créer une bouilloire avant d’entrer dans
notre boucle de jeu, et, une fois en jeu, d’appeler la fonction
faitNuage
à chaque itération de cette boucle pour avoir un ciel
bien dynamique. Nous retirons aussi toutes les instructions relatives
aux trois nuages de test qui nous avions créés auparavant:
import pygame
import random
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixel/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
COULOIRS_AERIENS = 5
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
INTERVALLE_NUAGES = 1000 # ms
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
##### Définition MOMENTALEATOIRE #####
def nouveauMomentAleatoire(intervalle):
return {
'momentSuivant': 0,
'max': intervalle,
'min': intervalle // 2
}
def suivant(momentAleatoire, maintenant):
momentAleatoire['momentSuivant'] = maintenant + random.randint(momentAleatoire['min'], momentAleatoire['max'])
def estExpire(momentAleatoire, maintenant):
return momentAleatoire['momentSuivant'] <= maintenant
##### Fin MOMENTALEATOIRE #####
##### Définition BOUILLOIRE #####
def nouvelleBouilloire(intervalle):
return {
'haut': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventHaut(ciel))),
'bas': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventBas(ciel))),
'autre': nouveauMomentAleatoire(intervalle),
'zones': {'haut', 'bas', 'autre'}
}
def faitNuage(bouilloire, maintenant):
for zone in bouilloire['zones']:
moment = bouilloire[zone]
if estExpire(moment, maintenant):
suivant(moment, maintenant)
if zone == 'haut':
nuage = nouveauNuageHaut(ciel, IMAGE_NUAGE)
elif zone == 'bas':
nuage = nouveauNuageBas(ciel, IMAGE_NUAGE)
else:
couloir = random.randint(1, COULOIRS_AERIENS)
nuage = nouveauNuage(ciel, couloir, IMAGE_NUAGE)
reveille(nuage)
ajouteEntite(scene, nuage)
##### Fin BOUILLOIRE #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
def enScene():
for acteur in acteurs(scene):
if acteur != oiseau and rectangle(acteur).right < 0:
enleveEntite(scene, acteur)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
bouilloire = nouvelleBouilloire(INTERVALLE_NUAGES)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu:
miseAJourScore(score, maintenant)
faitNuage(bouilloire, maintenant)
fenetre.fill(BLEU_CIEL)
enScene()
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Problème 4¶
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, il vous est demandé de générer de manière aléatoire un objet power-up dans un des couloirs du ciel. Pour l’instant, aucune interaction avec cet item n’est nécessaire. Flappy doit simplement passer au travers de l’objet.
Téléchargez l’image suivante dans votre sous-répertoire images:
Il ne peut y avoir, à tout moment, au plus qu’un seul power-up à l’écran.
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 05/11/2024-23h59. Si la deadline n’est pas respectée, vous aurez 0.
Pas d’issue pour Flappy¶
Dans la version actuelle de notre jeu, nous voyons clairement un front nuageux arriver en début de jeu. C’est ce qu’il nous faut pour les nuages de la zone centrale de l’écran, mais probablement pas ce que l’on veut pour les couloirs supérieur et inférieur de l’écran. Pour avoir, en permanence, les deux gros nuages au dessus et en dessous de Flappy, il fait faire deux choses:
modifier notre bouilloire pour qu’elle génère les nuages de manière différente lorsqu’on est en jeu (quand elle doit générer des nuages dans tous les couloirs aériens), ou quand on ne l’est pas (quand elle ne doit générer des nuages que dans les couloirs supérieur et inférieur).
Ceci est remarquablement simple: il nous suffit d’utiliser un opérateur ternaire pour sélectionner les
'zones'
concernées dans la bouclefor
defaitNuage
:for zone in bouilloire['zones'] if enJeu else {'haut', 'bas'}:peupler les couloirs supérieur et inférieur de nuages avant de commencer la boucle de jeu:
for x in range(0, FENETRE_LARGEUR + NUAGE_LARGEUR, NUAGE_LARGEUR): nuage1 = nouveauNuageHaut(ciel, IMAGE_NUAGE) pos = position(nuage1) place(nuage1, x, pos[1]) nuage2 = nouveauNuageBas(ciel, IMAGE_NUAGE) pos = position(nuage2) place(nuage2, x, pos[1]) ajouteEntite(scene, nuage1) ajouteEntite(scene, nuage2)
Comme faitNuage
a maintenant deux modes de génération « en
jeu »/ »pas en jeu », il nous suffit aussi, dans la boucle de jeu, de
« sortir » l’appel à cette fonction de la condition if enJeu:
.
Notre programme est maintenant:
import pygame
import random
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixel/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
COULOIRS_AERIENS = 5
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
INTERVALLE_NUAGES = 1000 # ms
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
##### Définition MOMENTALEATOIRE #####
def nouveauMomentAleatoire(intervalle):
return {
'momentSuivant': 0,
'max': intervalle,
'min': intervalle // 2
}
def suivant(momentAleatoire, maintenant):
momentAleatoire['momentSuivant'] = maintenant + random.randint(momentAleatoire['min'], momentAleatoire['max'])
def estExpire(momentAleatoire, maintenant):
return momentAleatoire['momentSuivant'] <= maintenant
##### Fin MOMENTALEATOIRE #####
##### Définition BOUILLOIRE #####
def nouvelleBouilloire(intervalle):
return {
'haut': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventHaut(ciel))),
'bas': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventBas(ciel))),
'autre': nouveauMomentAleatoire(intervalle),
'zones': {'haut', 'bas', 'autre'}
}
def faitNuage(bouilloire, maintenant):
for zone in bouilloire['zones'] if enJeu else {'haut', 'bas'}:
moment = bouilloire[zone]
if estExpire(moment, maintenant):
suivant(moment, maintenant)
if zone == 'haut':
nuage = nouveauNuageHaut(ciel, IMAGE_NUAGE)
elif zone == 'bas':
nuage = nouveauNuageBas(ciel, IMAGE_NUAGE)
else:
couloir = random.randint(1, COULOIRS_AERIENS)
nuage = nouveauNuage(ciel, couloir, IMAGE_NUAGE)
reveille(nuage)
ajouteEntite(scene, nuage)
##### Fin BOUILLOIRE #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
def enScene():
for acteur in acteurs(scene):
if acteur != oiseau and rectangle(acteur).right < 0:
enleveEntite(scene, acteur)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for x in range(0, FENETRE_LARGEUR + NUAGE_LARGEUR, NUAGE_LARGEUR):
nuage1 = nouveauNuageHaut(ciel, IMAGE_NUAGE)
pos = position(nuage1)
place(nuage1, x, pos[1])
nuage2 = nouveauNuageBas(ciel, IMAGE_NUAGE)
pos = position(nuage2)
place(nuage2, x, pos[1])
ajouteEntite(scene, nuage1)
ajouteEntite(scene, nuage2)
bouilloire = nouvelleBouilloire(INTERVALLE_NUAGES)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- traiter entrées joueurs
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu:
miseAJourScore(score, maintenant)
faitNuage(bouilloire, maintenant)
fenetre.fill(BLEU_CIEL)
enScene()
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Flappy est-il dans les nuages?¶
Nous allons maintenant détecter si Flappy touche des nuages. Pour ce faire, nous allons utiliser une méthode très simple: nous allons détecter si le cadre de limitation de Flappy possède une intersection non vide avec le cadre de limitation d’un ou plusieurs nuages.
Ici, nous avons beaucoup de chance, car il se trouve que Pygame fournit des facilités pour détecter ce genre d’intersections entre rectangles [5].
Vous ne devriez pas être surpris, car c’est une opération très courante dans les jeux!
Plus particulièrement, Pygame offre la commande collidelist
, qui
prend une liste de rectangles comme argument, et qui, lorsqu’appliquée
à un rectangle particulier, retourne l’index du premier rectangle de
la liste qui possède une intersection non vide avec le rectangle particulier.
Si aucune intersection n’est trouvée, -1
est retourné. C’est exactement ce
qu’il nous faut:
def collision():
return rectangle(oiseau).collidelist([rectangle(o) for o in acteurs(scene) if o != oiseau]) != -1
Ici, nous voyons, à nouveau, une compréhension de liste, mais cette
fois, avec un filtre: les éléments générés sont ceux pour lesquels la
condition if o != oiseau
est vraie.
Donc, cette fonction retourne True
si le cadre de limitation de
oiseau
touche le cadre de limitation d’au moins un nuage;
elle retourne False
sinon (pas d’intersection).
Notez que l’utilisation du filtre dans la compréhension de liste est capital dans notre cas: sans ce filtre, Flappy serait ajouté dans la liste et, à coup sûr, il possède une intersection avec lui même.
Il nous suffit alors de tester, dans la boucle principale, si nous avons des collisions:
import pygame
import random
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixel/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
COULOIRS_AERIENS = 5
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
INTERVALLE_NUAGES = 1000 # ms
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
##### Définition MOMENTALEATOIRE #####
def nouveauMomentAleatoire(intervalle):
return {
'momentSuivant': 0,
'max': intervalle,
'min': intervalle // 2
}
def suivant(momentAleatoire, maintenant):
momentAleatoire['momentSuivant'] = maintenant + random.randint(momentAleatoire['min'], momentAleatoire['max'])
def estExpire(momentAleatoire, maintenant):
return momentAleatoire['momentSuivant'] <= maintenant
##### Fin MOMENTALEATOIRE #####
##### Définition BOUILLOIRE #####
def nouvelleBouilloire(intervalle):
return {
'haut': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventHaut(ciel))),
'bas': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventBas(ciel))),
'autre': nouveauMomentAleatoire(intervalle),
'zones': {'haut', 'bas', 'autre'}
}
def faitNuage(bouilloire, maintenant):
for zone in bouilloire['zones'] if enJeu else {'haut', 'bas'}:
moment = bouilloire[zone]
if estExpire(moment, maintenant):
suivant(moment, maintenant)
if zone == 'haut':
nuage = nouveauNuageHaut(ciel, IMAGE_NUAGE)
elif zone == 'bas':
nuage = nouveauNuageBas(ciel, IMAGE_NUAGE)
else:
couloir = random.randint(1, COULOIRS_AERIENS)
nuage = nouveauNuage(ciel, couloir, IMAGE_NUAGE)
reveille(nuage)
ajouteEntite(scene, nuage)
##### Fin BOUILLOIRE #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
def enScene():
for acteur in acteurs(scene):
if acteur != oiseau and rectangle(acteur).right < 0:
enleveEntite(scene, acteur)
def collision():
return rectangle(oiseau).collidelist([rectangle(o) for o in acteurs(scene) if o != oiseau]) != -1
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for x in range(0, FENETRE_LARGEUR + NUAGE_LARGEUR, NUAGE_LARGEUR):
nuage1 = nouveauNuageHaut(ciel, IMAGE_NUAGE)
pos = position(nuage1)
place(nuage1, x, pos[1])
nuage2 = nouveauNuageBas(ciel, IMAGE_NUAGE)
pos = position(nuage2)
place(nuage2, x, pos[1])
ajouteEntite(scene, nuage1)
ajouteEntite(scene, nuage2)
bouilloire = nouvelleBouilloire(INTERVALLE_NUAGES)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- traiter entrées joueurs
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu:
miseAJourScore(score, maintenant)
if collision():
print ("collision")
faitNuage(bouilloire, maintenant)
fenetre.fill(BLEU_CIEL)
enScene()
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Notez que notre méthode de détection de collisions est un peu trop
simple: les cadres de limitation des entités peuvent très bien être en
collision, alors que les images ne le sont pas (voir image
ci-dessous). Dans ce cas, notre fonction collision
déclare quand
même qu’une collision s’est produite.
Nous laissons comme exercice la correction de cette imperfection ;-)
Pauvre Flappy!¶
Notre sympathique Flappy a un petit problème: il est allergique à l’eau! Et lorsqu’il touche des nuages, ça lui fait perdre la santé.
Nous définissons donc un dictionnaire et des fonctions pour représenter et manipuler la santé de Flappy:
VIEILLISEMENT = 10 # /s
##### Définition SANTE #####
def nouvelleSante(vieillissement):
return {
'taux': vieillissement,
'derniereEvaluation': 0,
'valeur': 100
}
def enVie(sante):
return sante['valeur'] > 0
def bilan(sante, moment):
sante['derniereEvaluation'] = moment
def vieillir(sante, maintenant):
sante['valeur'] -= sante['taux'] * (maintenant - sante['derniereEvaluation']) / 1000
sante['derniereEvaluation'] = maintenant
def forme(sante):
return sante['valeur']
##### Fin SANTE #####
La 'valeur'
de la santé de Flappy est en fait un pourcentage: Flappy
possède 100% de santé lorsqu’il est en pleine forme.
'taux'
définit la vitesse à laquelle la santé de Flappy se
détériore.
La fonction vieillir
fait perdre de la santé à Flappy. La
quantité de santé dépend, bien entendu, de la vitesse de
déterioration de la santé, et du temps de vieillissement qui est
calculé depuis la dernière évaluation de la santé.
La fonction bilan
permet d’indiquer que Flappy a subi une
évaluation de santé, sans nécessairement vieillir.
La fonction enVie
indique si Flappy a encore de la santé, et la
fonction forme
retourne la valeur de cette santé.
Avec ces outils, nous pouvons maintenant écrire une fonction qui fait évoluer la santé de Flappy:
def evolueSante():
rect_oiseau = rectangle(oiseau)
if rect_oiseau.top < 0 or rect_oiseau.bottom > FENETRE_HAUTEUR or collision():
vieillir(sante, maintenant)
else:
bilan(sante, maintenant)
Simplement, Flappy vieillit s’il est situé soit dans un des gros nuages au dessus ou en dessous de l’écran, soit s’il touche un des nuages présents à l’écran.
Il nous suffit alors de créer une nouvelle valeur de santé pour Flappy avant
d’entrer dans la boucle de jeu, et d’appeler la fonction
evolueSante
depuis la boucle de jeu. Une fois la santé de Flappy
évaluée, nous l’imprimons pour nous assurer que tout fonctionne comme
prévu, et il ne nous reste plus qu’à déterminer si Flappy est toujours
en vie, car si ce n’est plus le cas, alors le jeu doit se terminer.
Finalement, notez que lors de la création de la santé de Flappy, le
moment de la dernière évaluation est arbitrairement mis à 0
. Comme
nous ne savons pas pendant combien de temps le joueur va attendre
avant de commencer le jeu, nous faisons un bilan de santé de Flappy
lorsque nous détectons le commencement du jeu, afin d’assurer que
Flappy commence le jeu en pleine forme:
import pygame
import random
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixel/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
COULOIRS_AERIENS = 5
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
INTERVALLE_NUAGES = 1000 # ms
VIEILLISEMENT = 10 # /s
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
##### Définition MOMENTALEATOIRE #####
def nouveauMomentAleatoire(intervalle):
return {
'momentSuivant': 0,
'max': intervalle,
'min': intervalle // 2
}
def suivant(momentAleatoire, maintenant):
momentAleatoire['momentSuivant'] = maintenant + random.randint(momentAleatoire['min'], momentAleatoire['max'])
def estExpire(momentAleatoire, maintenant):
return momentAleatoire['momentSuivant'] <= maintenant
##### Fin MOMENTALEATOIRE #####
##### Définition BOUILLOIRE #####
def nouvelleBouilloire(intervalle):
return {
'haut': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventHaut(ciel))),
'bas': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventBas(ciel))),
'autre': nouveauMomentAleatoire(intervalle),
'zones': {'haut', 'bas', 'autre'}
}
def faitNuage(bouilloire, maintenant):
for zone in bouilloire['zones'] if enJeu else {'haut', 'bas'}:
moment = bouilloire[zone]
if estExpire(moment, maintenant):
suivant(moment, maintenant)
if zone == 'haut':
nuage = nouveauNuageHaut(ciel, IMAGE_NUAGE)
elif zone == 'bas':
nuage = nouveauNuageBas(ciel, IMAGE_NUAGE)
else:
couloir = random.randint(1, COULOIRS_AERIENS)
nuage = nouveauNuage(ciel, couloir, IMAGE_NUAGE)
reveille(nuage)
ajouteEntite(scene, nuage)
##### Fin BOUILLOIRE #####
##### Définition SANTE #####
def nouvelleSante(vieillissement):
return {
'taux': vieillissement,
'derniereEvaluation': 0,
'valeur': 100
}
def enVie(sante):
return sante['valeur'] > 0
def bilan(sante, moment):
sante['derniereEvaluation'] = moment
def vieillir(sante, maintenant):
sante['valeur'] -= sante['taux'] * (maintenant - sante['derniereEvaluation']) / 1000
sante['derniereEvaluation'] = maintenant
def forme(sante):
return sante['valeur']
##### Fin SANTE #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
bilan(sante, pygame.time.get_ticks())
def enScene():
for acteur in acteurs(scene):
if acteur != oiseau and rectangle(acteur).right < 0:
enleveEntite(scene, acteur)
def collision():
return rectangle(oiseau).collidelist([rectangle(o) for o in acteurs(scene) if o != oiseau]) != -1
def evolueSante():
rect_oiseau = rectangle(oiseau)
if rect_oiseau.top < 0 or rect_oiseau.bottom > FENETRE_HAUTEUR or collision():
vieillir(sante, maintenant)
else:
bilan(sante, maintenant)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
sante = nouvelleSante(VIEILLISEMENT)
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for x in range(0, FENETRE_LARGEUR + NUAGE_LARGEUR, NUAGE_LARGEUR):
nuage1 = nouveauNuageHaut(ciel, IMAGE_NUAGE)
pos = position(nuage1)
place(nuage1, x, pos[1])
nuage2 = nouveauNuageBas(ciel, IMAGE_NUAGE)
pos = position(nuage2)
place(nuage2, x, pos[1])
ajouteEntite(scene, nuage1)
ajouteEntite(scene, nuage2)
bouilloire = nouvelleBouilloire(INTERVALLE_NUAGES)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
while not fini:
# --- Traiter entrées joueur
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu:
miseAJourScore(score, maintenant)
evolueSante()
print(forme(sante))
if not enVie(sante):
fini = True
faitNuage(bouilloire, maintenant)
fenetre.fill(BLEU_CIEL)
enScene()
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
temps.tick(50)
pygame.display.quit()
pygame.quit()
exit()
Notez que nous avons omis la détection de collisions explicite dans la
boucle principale, car celle-ci se fait maintenant dans la fonction
evolueSante
.
Montrer la santé de Flappy¶
Nous allons créer un indicateur pour indiquer l’état de santé de Flappy. Cet indicateur sera simplement un rectangle coloré dont une proportion égale au pourcentage de santé restant à Flappy sera verte, et le reste sera rouge.
Pour rendre notre code un peu plus général, et donc plus enclin à
pouvoir être réutilisé dans un autre projet, nous allons passer, lors
de la construction de notre indicateur, une fonction permettant
d’obtenir le pourcentage de vert. Utiliser une fonction comme argument
d’une autre fonction, et évaluer/appeler cette fonction-argument
ultérieurement est tout à fait possible, mais passer des arguments à
cette fonction-argument est un peu compliqué. Cependant, notre
fonction forme
, celle qui retourne précisément le pourcentage de
santé qu’il reste à Flappy, prend comme argument une sante
. Pour
éviter cette complication, il nous suffit de définir une fonction
« d’emballage », qui ne prend aucun argument, et d’utiliser cette
fonction lors de la création de notre indicateur:
def valeurSante():
return forme(sante)
Nous pouvons maintenant écrire le code pour notre indicateur de santé:
BARRE_LARGEUR = 50
BARRE_HAUTEUR = 20
BARRE_GAUCHE = FENETRE_LARGEUR - BARRE_LARGEUR - 50
BARRE_DESSUS = 50
##### Définition BARRE #####
def nouvelleBarre(rect, fct_valeur):
return {
'rect': rect,
'fct': fct_valeur
}
def montre(barre, ecran):
restant = barre['fct']()
if restant < 0: restant = 0
rect = barre['rect']
largeur_rouge = int(rect.width * (100 - restant) / 100)
if restant < 100:
pygame.draw.rect(ecran, (255, 0, 0), (rect.left, rect.top, largeur_rouge, rect.height))
if restant > 0:
pygame.draw.rect(ecran, (0, 255, 0), (rect.left+ largeur_rouge, rect.top, rect.width - largeur_rouge, rect.height))
##### Fin BARRE #####
Notre indicateur (barre) consiste en en rectangle définissant sa géométrie et une fonction qui indique le pourcentage de vert.
Le nom d’une fonction, par exemple valeurSante
(sans les ()
)
représente la fonction et peut être stocké dans une variable
en vue d’une utilisation future. Par exemple:
f = valeurSante
Dans ce cas, la commande f()
est complètement équivalente à
valeurSante()
.
Le fonctionnement de montre
devient alors clair: si
valeurSante
est passé comme deuxième argument à nouvelleBarre
,
barre['fct']()
a alors le même effet que valeurSante()
.
La valeur retournée par forme(sante)
, via valeurSante
peut
être négative. Si c’est le cas, nous forçons cette valeur à 0
.
De là, il est aisé de calculer la partie rouge de notre barre ((1 - pourcentage de vert)% de la barre), et s’il y a du rouge, nous le dessinons, suivi du vert s’il y en a.
Il ne nous reste plus qu’à créer une barre:
BARRE_LARGEUR = 50
BARRE_HAUTEUR = 20
BARRE_GAUCHE = FENETRE_LARGEUR - BARRE_LARGEUR - 50
BARRE_DESSUS = 50
barre = nouvelleBarre(pygame.Rect(BARRE_GAUCHE, BARRE_DESSUS, BARRE_LARGEUR, BARRE_HAUTEUR), valeurSante)
et à afficher cette barre, dans la boucle de jeu, si un jeu est en cours.
Après avoir enlevé les instructions d’impression au terminal de la valeur de la santé de Flappy, et ajouté un message « Game over » lorsque le jeu se termine, notre programme est:
import pygame
import random
BLEU_CIEL = (135, 206, 250)
ORANGE = (255, 165, 0)
FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 550
FLAPPY_LARGEUR = 60
FLAPPY_HAUTEUR = 51
NUAGE_LARGEUR = 127
NUAGE_HAUTEUR = 75
ACC_CHUTE = (0, FENETRE_HAUTEUR) # pixel/s^2
DEPLACEMENT_HAUT = -FENETRE_HAUTEUR / 25
VITESSE_HORIZONTALE = 4 # m/s
COULOIRS_AERIENS = 5
NUAGE_VITESSE_MIN = FENETRE_LARGEUR // 10 # pixel/s
NUAGE_VITESSE_MAX = FENETRE_LARGEUR // 2
INTERVALLE_NUAGES = 1000 # ms
VIEILLISEMENT = 10 # /s
BARRE_LARGEUR = 50
BARRE_HAUTEUR = 20
BARRE_GAUCHE = FENETRE_LARGEUR - BARRE_LARGEUR - 50
BARRE_DESSUS = 50
##### Définition MOUVEMENT #####
def mouvement(nom, duree):
return (nom, duree) # durée en msec
def nomMouvement(mvt):
return mvt[0]
def dureeMouvement(mvt):
return mvt[1]
##### Fin MOUVEMENT #####
##### Définition ANIMATION #####
def nouvelleAnimation():
return {
'boucle':False,
'repetition': 0,
'momentMouvementSuivant':None,
'indexMouvement':None,
'choregraphie':[] # liste de mouvements
}
def repete(animation, fois):
animation['repetition'] = fois
animation['boucle'] = False
def enBoucle(animation):
animation['boucle'] = True
def ajouteMouvement(animation, mvt):
animation['choregraphie'].append(mvt)
def mouvementActuel(animation):
if animation['indexMouvement'] == None:
return None
else:
return nomMouvement(animation['choregraphie'][animation['indexMouvement']])
def commenceMouvement(animation, index):
animation['indexMouvement'] = index
animation['momentMouvementSuivant'] = pygame.time.get_ticks() + dureeMouvement(animation['choregraphie'][index])
def commence(animation):
commenceMouvement(animation, 0)
def arrete(animation):
animation['indexMouvement'] = None
def anime(animation):
if animation['indexMouvement'] == None:
commence(animation)
elif animation['momentMouvementSuivant'] <= pygame.time.get_ticks():
if animation['indexMouvement'] == len(animation['choregraphie']) - 1:
if animation['boucle']:
commence(animation)
else:
if animation['repetition'] > 0:
animation['repetition'] -= 1
commence(animation)
else:
arrete(animation)
else:
commenceMouvement(animation, animation['indexMouvement'] + 1)
##### Fin ANIMATION #####
##### Définition ENTITE #####
def nouvelleEntite():
return {
'visible':False,
'position': [0, 0],
'vitesse': [0, 0],
'acceleration': [0, 0],
'momentDeplacement': 0,
'imageAffichee':None,
'poses': {}, # dictionnaire de nom:image
'animationActuelle':None,
'animations':{} #dictionnaire de nom:animation
}
def visible(entite):
entite['visible'] = True
def invisible(entite):
entite['visible'] = False
def estVisible(entite):
return entite['visible']
def place(entite, x, y):
entite['position'][0] = x
entite['position'][1] = y
def vitesse(entite, vx, vy):
entite['vitesse'][0] = vx
entite['vitesse'][1] = vy
def acceleration(entite, ax, ay):
entite['acceleration'][0] = ax
entite['acceleration'][1] = ay
def deplace(entite, maintenant):
dt = (maintenant - entite['momentDeplacement']) / 1000
# mise à jour vitesse
entite['vitesse'][0] += entite['acceleration'][0] * dt
entite['vitesse'][1] += entite['acceleration'][1] * dt
# mise à jour position
entite['position'][0] += entite['vitesse'][0] * dt
entite['position'][1] += entite['vitesse'][1] * dt
# mise à jour moment de déplacement
entite['momentDeplacement'] = maintenant
def reveille(entite):
entite['momentDeplacement'] = pygame.time.get_ticks()
def position(entite):
return entite['position']
def ajoutePose(entite, nom, image):
entite['poses'][nom] = image
def prendsPose(entite, nom_pose):
entite['imageAffichee'] = entite['poses'][nom_pose]
visible(entite)
def dessine(entite, ecran):
ecran.blit(entite['imageAffichee'], entite['position'])
def commenceAnimation(entite, nomAnimation, fois = 1):
entite['animationActuelle'] = entite['animations'][nomAnimation]
if fois == 0:
enBoucle(entite['animationActuelle'])
else:
repete(entite['animationActuelle'], fois - 1)
visible(entite)
def arreteAnimation(entite):
arrete(entite['animationActuelle'])
entite['animationActuelle'] = None
def ajouteAnimation(entite, nom, animation):
entite['animations'][nom] = animation
def estEnAnimation(entite):
return entite['animationActuelle'] != None
def rectangle(entite):
return entite['imageAffichee'].get_rect().move(entite['position'][0], entite['position'][1])
##### Fin ENTITE #####
##### Définition SCORE #####
def score():
return {
'valeur': 0,
'derniereMiseAJour': 0
}
def miseAJourScore(score, maintenant):
dt = (maintenant - score['derniereMiseAJour']) / 1000
score['derniereMiseAJour'] = maintenant
score['valeur'] += VITESSE_HORIZONTALE * dt
def resultat(score):
return int(score['valeur'])
def reinitialiser(score):
score['valeur'] = 0
score['derniereMiseAJour'] = pygame.time.get_ticks()
##### Fin SCORE #####
##### Définition SCENE #####
def nouvelleScene():
return {
'acteurs': []
}
def ajouteEntite(scene, entite):
scene['acteurs'].append(entite)
def enleveEntite(scene, entite):
acteurs = scene['acteurs']
if entite in acteurs:
acteurs.remove(entite)
def acteurs(scene):
return list(scene['acteurs'])
def miseAJour(scene, maintenant):
maScene = acteurs(scene)
for entite in maScene:
deplace(entite, maintenant)
def affiche(scene, ecran):
entites = acteurs(scene)
for objet in entites:
if estVisible(objet):
if estEnAnimation(objet):
animationActuelle = objet['animationActuelle']
poseActuelle = mouvementActuel(animationActuelle)
anime(animationActuelle)
nouvellePose = mouvementActuel(animationActuelle)
if nouvellePose == None:
objet['animationActuelle'] = None
prendsPose(objet, poseActuelle)
else:
prendsPose(objet, nouvellePose)
dessine(objet, ecran)
##### Fin SCENE #####
##### Définition CIEL #####
def nouveauCiel(nombreCouloirs):
random.seed()
taille_couloir = FENETRE_HAUTEUR // (nombreCouloirs + 1)
demi_couloir = taille_couloir // 2
return {
'couloirs': [0] + [demi_couloir + n*taille_couloir for n in range(nombreCouloirs + 1)],
'vents': [0] * (nombreCouloirs + 2),
'nombreCouloirs': nombreCouloirs,
'tailleCouloir': taille_couloir
}
def changeVitesseVent(ciel, couloir, vitesse):
ciel['vents'][couloir] = -vitesse
def changeVentHaut(ciel, vitesse):
changeVitesseVent(ciel, 0, vitesse)
def changeVentBas(ciel, vitesse):
changeVitesseVent(ciel, ciel['nombreCouloirs'] + 1, vitesse)
def vitesseVent(ciel, couloir):
return ciel['vents'][couloir]
def ventHaut(ciel):
return vitesseVent(ciel, 0)
def ventBas(ciel):
return vitesseVent(ciel, ciel['nombreCouloirs'] + 1)
def debutCouloir(ciel, couloir):
return ciel['couloirs'][couloir]
def debutHaut(ciel):
return debutCouloir(ciel, 0)
def debutBas(ciel):
return debutCouloir(ciel, ciel['nombreCouloirs'] + 1)
def rangeCouloirs(ciel):
return range(1, ciel['nombreCouloirs'] + 1)
def nouveauNuage(ciel, couloir, image):
nuage = nouvelleEntite()
ajoutePose(nuage, 'nuage', image)
prendsPose(nuage, 'nuage')
rect = rectangle(nuage)
debut = debutCouloir(ciel, couloir)
fin = debutCouloir(ciel, couloir + 1) - 1 if couloir < ciel['nombreCouloirs'] + 1 else FENETRE_HAUTEUR - 1
if couloir == 0:
rect.bottom = random.randint((debut + fin) // 2, fin)
vitesse(nuage, ventHaut(ciel), 0)
elif couloir == ciel['nombreCouloirs'] + 1:
rect.top = random.randint(debut, (debut + fin) // 2)
vitesse(nuage, ventBas(ciel), 0)
else:
rect.top = random.randint(debut, fin - rect.height)
vitesse(nuage, vitesseVent(ciel, couloir), 0)
y = rect.top
place(nuage, FENETRE_LARGEUR, y)
return nuage
def nouveauNuageHaut(ciel, image):
return nouveauNuage(ciel, 0, image)
def nouveauNuageBas(ciel, image):
return nouveauNuage(ciel, ciel['nombreCouloirs'] + 1, image)
##### Fin CIEL #####
##### Définition MOMENTALEATOIRE #####
def nouveauMomentAleatoire(intervalle):
return {
'momentSuivant': 0,
'max': intervalle,
'min': intervalle // 2
}
def suivant(momentAleatoire, maintenant):
momentAleatoire['momentSuivant'] = maintenant + random.randint(momentAleatoire['min'], momentAleatoire['max'])
def estExpire(momentAleatoire, maintenant):
return momentAleatoire['momentSuivant'] <= maintenant
##### Fin MOMENTALEATOIRE #####
##### Définition BOUILLOIRE #####
def nouvelleBouilloire(intervalle):
return {
'haut': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventHaut(ciel))),
'bas': nouveauMomentAleatoire(abs(1000 * NUAGE_LARGEUR // ventBas(ciel))),
'autre': nouveauMomentAleatoire(intervalle),
'zones': {'haut', 'bas', 'autre'}
}
def faitNuage(bouilloire, maintenant):
for zone in bouilloire['zones'] if enJeu else {'haut', 'bas'}:
moment = bouilloire[zone]
if estExpire(moment, maintenant):
suivant(moment, maintenant)
if zone == 'haut':
nuage = nouveauNuageHaut(ciel, IMAGE_NUAGE)
elif zone == 'bas':
nuage = nouveauNuageBas(ciel, IMAGE_NUAGE)
else:
couloir = random.randint(1, COULOIRS_AERIENS)
nuage = nouveauNuage(ciel, couloir, IMAGE_NUAGE)
reveille(nuage)
ajouteEntite(scene, nuage)
##### Fin BOUILLOIRE #####
##### Définition SANTE #####
def nouvelleSante(vieillissement):
return {
'taux': vieillissement,
'derniereEvaluation': 0,
'valeur': 100
}
def enVie(sante):
return sante['valeur'] > 0
def bilan(sante, moment):
sante['derniereEvaluation'] = moment
def vieillir(sante, maintenant):
sante['valeur'] -= sante['taux'] * (maintenant - sante['derniereEvaluation']) / 1000
sante['derniereEvaluation'] = maintenant
def forme(sante):
return sante['valeur']
##### Fin SANTE #####
##### Définition BARRE #####
def nouvelleBarre(rect, fct_valeur):
return {
'rect': rect,
'fct': fct_valeur
}
def montre(barre, ecran):
restant = barre['fct']()
if restant < 0: restant = 0
rect = barre['rect']
largeur_rouge = int(rect.width * (100 - restant) / 100)
if restant < 100:
pygame.draw.rect(ecran, (255, 0, 0), (rect.left, rect.top, largeur_rouge, rect.height))
if restant > 0:
pygame.draw.rect(ecran, (0, 255, 0), (rect.left+ largeur_rouge, rect.top, rect.width - largeur_rouge, rect.height))
##### Fin BARRE #####
def traite_entrees():
global fini, enJeu
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
exit()
elif evenement.type == pygame.KEYDOWN:
if enJeu:
positionOiseau = position(oiseau)
place(oiseau, positionOiseau[0], positionOiseau[1] + DEPLACEMENT_HAUT)
vitesse(oiseau, 0, 0)
if not estEnAnimation(oiseau):
commenceAnimation(oiseau, 'vol')
else:
enJeu = True
arreteAnimation(oiseau)
prendsPose(oiseau, 'AILE_MILIEU')
acceleration(oiseau, ACC_CHUTE[0], ACC_CHUTE[1])
reinitialiser(score)
bilan(sante, pygame.time.get_ticks())
def enScene():
for acteur in acteurs(scene):
if acteur != oiseau and rectangle(acteur).right < 0:
enleveEntite(scene, acteur)
def collision():
return rectangle(oiseau).collidelist([rectangle(o) for o in acteurs(scene) if o != oiseau]) != -1
def evolueSante():
rect_oiseau = rectangle(oiseau)
if rect_oiseau.top < 0 or rect_oiseau.bottom > FENETRE_HAUTEUR or collision():
vieillir(sante, maintenant)
else:
bilan(sante, maintenant)
def valeurSante():
return forme(sante)
pygame.init()
fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)
pygame.display.set_caption('FLAPPY')
oiseau = nouvelleEntite()
for nom_image, nom_fichier in (('AILE_HAUTE','bird_wing_up.png'),
('AILE_MILIEU','bird_wing_mid.png'),
('AILE_BASSE', 'bird_wing_down.png')):
chemin = 'images/' + nom_fichier
image = pygame.image.load(chemin).convert_alpha(fenetre)
image = pygame.transform.scale(image, (FLAPPY_LARGEUR, FLAPPY_HAUTEUR))
ajoutePose(oiseau, nom_image, image)
sante = nouvelleSante(VIEILLISEMENT)
IMAGE_NUAGE = pygame.image.load('images/cloud.png').convert_alpha(fenetre)
IMAGE_NUAGE = pygame.transform.scale(IMAGE_NUAGE, (NUAGE_LARGEUR, NUAGE_HAUTEUR))
animation = nouvelleAnimation()
ajouteMouvement(animation, mouvement('AILE_HAUTE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteMouvement(animation, mouvement('AILE_BASSE', 80))
ajouteMouvement(animation, mouvement('AILE_MILIEU', 80))
ajouteAnimation(oiseau, 'vol', animation)
place(oiseau, 50, 50)
scene = nouvelleScene()
ajouteEntite(scene, oiseau)
commenceAnimation(oiseau, 'vol', 0)
ciel = nouveauCiel(COULOIRS_AERIENS)
changeVentHaut(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
changeVentBas(ciel, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for coul in rangeCouloirs(ciel):
changeVitesseVent(ciel, coul, random.randint(NUAGE_VITESSE_MIN, NUAGE_VITESSE_MAX))
for x in range(0, FENETRE_LARGEUR + NUAGE_LARGEUR, NUAGE_LARGEUR):
nuage1 = nouveauNuageHaut(ciel, IMAGE_NUAGE)
pos = position(nuage1)
place(nuage1, x, pos[1])
nuage2 = nouveauNuageBas(ciel, IMAGE_NUAGE)
pos = position(nuage2)
place(nuage2, x, pos[1])
ajouteEntite(scene, nuage1)
ajouteEntite(scene, nuage2)
bouilloire = nouvelleBouilloire(INTERVALLE_NUAGES)
police_caractere = pygame.font.SysFont('monospace', 24, True)
message = police_caractere.render("N'importe quelle touche pour commencer/voler", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("N'importe quelle touche pour commencer/voler")
fini = False
enJeu = False
score = score()
temps = pygame.time.Clock()
barre = nouvelleBarre(pygame.Rect(BARRE_GAUCHE, BARRE_DESSUS, BARRE_LARGEUR, BARRE_HAUTEUR), valeurSante)
while not fini:
# --- Traiter entrées joueur
traite_entrees()
maintenant = pygame.time.get_ticks()
miseAJour(scene, maintenant)
if enJeu:
miseAJourScore(score, maintenant)
evolueSante()
if not enVie(sante):
fini = True
faitNuage(bouilloire, maintenant)
fenetre.fill(BLEU_CIEL)
enScene()
affiche(scene, fenetre)
affichageScore = str(resultat(score)) + ' m'
marquoir = police_caractere.render(affichageScore, True, ORANGE)
marquoirLargeur, marquoirHauteur = police_caractere.size(affichageScore)
fenetre.blit(marquoir, ((FENETRE_LARGEUR - marquoirLargeur) // 2, 10))
if not enJeu:
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
else:
montre(barre, fenetre)
pygame.display.flip()
temps.tick(50)
message = police_caractere.render("GAME OVER", True, ORANGE)
messageLargeur, messageHauteur = police_caractere.size("GAME OVER")
fenetre.blit(message, ((FENETRE_LARGEUR - messageLargeur) // 2, (FENETRE_HAUTEUR - messageHauteur) // 2))
pygame.display.flip()
pygame.time.wait(5000)
pygame.display.quit()
pygame.quit()
exit()
Ajouter de la musique à Flappy¶
Votre jeu actuel, bien qu’il soit très élégant, manque un peu d’animations sonores…
Nous allons corriger cela et ajouter une musique de fond ainsi que divers bruitages.
Il faut savoir que Pygame dispose de deux modules distincts pour gérer des sons: le module Mixer et le module Music. Dans notre cas, nous allons nous limiter uniquement au module Mixer. En effet, le module Music, est particulièrement adapté pour une gestion plus avancée des musiques (fichiers plus volumineux, streaming, …). Il dépasse le cadre du premier projet d’informatique. Le module Mixer, quant à lui, est le module général utilisé pour la gestion des sons avec Pygame.
Utilisation du module Mixer¶
Nous souhaitons ajouter une musique de fond qui se répète
indéfiniment pendant que l’utilisateur joue. De plus, nous
voulons aussi que, lorsque Flappy touche un nuage (et donc perd sa
santé), un bruitage bref représentant sa terrible souffrance soit joué.
Nous allons utiliser les deux fichiers suivants:
background_music.wav
et flappy_cri.wav
.
Premièrement, il est nécessaire d’initialiser le module Mixer en
appelant la méthode pygame.mixer.init()
. Ensuite, vous devez créer
un objet Sound
, en précisant le fichier audio voulu. Notez que,
comme pour les images, il est nécessaire de préciser le chemin
absolu/relatif du fichier audio:
musique = pygame.mixer.Sound("sons/background_music.wav")
La variable musique
correspond maintenant à un objet qui contient
toutes les méthodes qui vont permettre de jouer le son:
musique.play() # lance la lecture du fichier audio
musique.stop() # stoppe la lecture du fichier audio
Pour répéter la musique en fond indéfiniment, il est nécessaire de
préciser l’argument loops
de la méthode play()
à -1
. Notre
code devient alors:
pygame.mixer.init() # NOUVEAU
musique = pygame.mixer.Sound("sons/background_music.wav") # NOUVEAU
musique.play(loops=-1) # NOUVEAU
while not fini:
#...
#...
musique.stop() # NOUVEAU
pygame.time.wait(5000)
pygame.display.quit()
pygame.quit()
Essayez de choisir des fichiers au format .wav ou .ogg, les fichiers .mp3 et les .wma peuvent engendrer des incompatibilités avec Pygame. Vous pouvez convertir n’importe quel fichier mp3 en wav grâce au site web suivant: <https://convertio.co/fr/mp3-wav/>
Jouez plusieurs sons simultanément¶
Nous voulons maintenant faire crier Flappy lorsqu’il perd sa santé. Il est possible de déclarer une nouvelle variable comme pour l’exemple précédent. Cependant, si vous voulez jouer plusieurs sons simultanément, cette méthode n’est pas optimale. En effet, si vous voulez jouer 5 sons différents, il sera nécessaire de déclarer 5 variables différentes…
A la place, nous allons utiliser un dictionnaire. Chaque clé du
dictionnaire représente le son à jouer. Sa valeur associée est un
objet Sound
permettant d’interagir directement avec le son. Notre
extrait de code précédent est donc modifié comme tel:
pygame.mixer.init()
sons = {}
sons['cri'] = pygame.mixer.Sound("sons/flappy_cri.wav")
sons['musique'] = pygame.mixer.Sound("sons/background_music.wav")
sons['musique'].play(loops=-1)
while not fini:
#...
#...
sons['musique'].stop()
pygame.time.wait(5000)
pygame.display.quit()
pygame.quit()
Il ne nous reste plus qu’à implémenter le code relatif au cri de Flappy lorsqu’il perd de la vie (collision avec des nuages). Néanmoins, il existe une petite subtilité concernant cette partie. A chaque fois que Flappy entre en collision avec un nuage, le son est joué. Cependant, si Flappy touche plusieurs nuages dans un intervalle de temps inférieur à la durée du bruitage, il peut y avoir un (léger) décalage sonore. Dans notre cas, cet effet est assez difficile à discerner vu que le bruitage actuel est de très courte durée.
Pour éviter cet effet, il est nécessaire de savoir si Flappy
crie déjà pendant une nouvelle collision. Pygame fournit la méthode
pygame.mixer.get_busy()
pour savoir si un son est en train
d’être joué. Cependant, cette fonction ne fonctionnera pas étant donné
que nous avons une musique de fond qui est jouée en boucle…
Fort heureusement, nous allons pouvoir contourner ce problème en jouant la musique dans des canaux distincts. Sans entrer dans les détails, il faut savoir que Pygame utilise, par défaut, un seul canal de sortie pour diffuser plusieurs sons. Nous allons modifier ce comportement en utilisant deux canaux différents: un pour la musique et l’autre pour les bruitages:
sons['cri'] = pygame.mixer.Sound("sons/flappy_cri.wav")
bruitages = pygame.mixer.Channel(5) # On utilise le canal numéro 5
Une fois ce nouveau canal défini, nous allons pouvoir appeler la
méthode get_busy()
sur notre variable bruitages
et
jouer le son correspondant au cri de Flappy. De cette manière, nous
n’aurons plus le problème précédent. Nous avons donc le code final:
def evolueSante():
rect_oiseau = rectangle(oiseau)
if rect_oiseau.top < 0 or rect_oiseau.bottom > FENETRE_HAUTEUR or collision():
if not bruitages.get_busy():# NOUVEAU
bruitages.play(sons['cri']) # NOUVEAU
vieillir(sante, maintenant)
else:
bilan(sante, maintenant)
Ceci n’est qu’une courte introduction sur la gestion des sons avec Pygame. Vous pouvez vous référez à la documentation officielle sur le composant Mixer pour gérer vos animations sonores.
Problème 5 (optionnel)¶
Ce problème est optionnel, vous n’aurez donc aucun bonus/malus. Il permettra uniquement de vous exercer encore un petit peu avant de vous lancer dans la suite de nos aventures.
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, intégrez le code que vous avez écrit pour le problème 4. Une fois cette étape terminée, modifiez votre code de sorte à garantir que le power-up ne soit jamais dans un nuage.
Ensuite, il vous est demandé de gérer l’interaction entre Flappy et le power-up. Chaque fois que Flappy touche un power-up, il récupère un dixième de sa santé. Le power-up doit disparaitre dès qu’il a été touché.
Vu qu’il s’agit d’un problème optionnel, vous ne devez pas le soumettre sur la plateforme de soumission.
Et maintenant?¶
Nous pourrions bien sûr continuer à améliorer notre jeu (retour à l’écran d’accueil après une partie, meilleur détection de collisions, etc ). Nous vous encourageons à faire ces exercices si vous le désirez.
Mais il est bien plus amusant de créer votre propre jeu.
Vous avez en effet maintenant beaucoup d’outils à votre disposition pour pouvoir laisser libre cours à votre imagination.
Nous vous conseillons cependant de choisir un jeu qui n’est pas trop compliqué, et dans tous les cas, d’adopter une approche de développement incrémental, comme illustré dans ce tutoriel: mieux vaut, à tout moment, avoir quelque chose de simple qui fonctionne, plutôt qu’un tas de lignes de code cassées, promettant un fantôme impressionnant!
Mais quel jeu pouvez-vous bien faire? Votre imagination est votre principale limite. Vous pouvez implementer une version d’un jeu d’arcade simple (snake, space invaders, asteroid, frogger, highway, pooyan, etc), un jeu de stratégie (puissance 4, 2048, etc), ou bien une version simplifiée d’un jeu installé sur votre téléphone ou console de jeu.
Avertissement
Avant de vous lancer dans votre jeu, merci de nous contacter pour en discuter. Si vous n’avez pas d’idée, contactez-nous également via discord/email. A noter que vous pouvez réutiliser des parties de code de ce tutoriel, mais pas du code trouvé sur Internet.
Attention que votre soumission est limitée à 50MB (fichier compressé). Aussi, vous ne pouvez pas utiliser de librairies autres que celles utilisées dans le tutoriel. La résolution maximale de votre fenêtre est de 1280x1024 et nous interdisons le mode plein écran. Finalement, il est de votre devoir de vous assurer que votre code utilise des chemins relatifs pour accéder à vos fichiers.
Si vous avez des questions ou rencontrez des difficultés, n’hésitez pas à vous adresser aux encadrants. N’oubliez pas que les documentations de Python et de Pygame sont également disponibles en ligne. Et pensez à sauvegarder fréquemment votre travail, de façon à pouvoir revenir en arrière si nécessaire.
Bon travail et, surtout, bon amusement!