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:

Flappy

Flappy.

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:

Flappy 2

Flappy pose 2.

Flappy 3

Flappy pose 3.

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].

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 23/10/2023-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.

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])
cadre de limitation de Flappy

Cadre de limitation de l’image de Flappy.

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:

Structure du ciel

Structure de notre ciel à l’écran.

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.

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:

nuage

Notre image de nuage.

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:

power-up

Image du power-up.

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 30/10/2023-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 boucle for de faitNuage:

    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].

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.

fausse collision

Fausse collision.

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()

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.

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!