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 20/10/2025-23h59. Si la deadline n’est pas respectée, vous aurez 0.