Wireframe mag : améliorer le code des explosions

La fondation RaspberryPi publie quelques magazines avec l’intention de rendre l’informatique accessible à tous. Wireframe Magazine est une publication lancée en 2018 dédiée à la création de jeux vidéo. Chaque numéro a un petit chapitre dédié à la bibliothèque PyGame Zero qui permet de développer des jeux en Python.

Le numéro 1 propose un code pour reproduire en Python les explosions (à l’époque) révolutionnaires d’un vieux jeu sasféplu, Defender. Le code est disponible sur GitHub et vous pouvez vous procurer Wireframe 1 en pdf pour les explications.

Ce code illustre avec pédagogie l’explosion. Mais en tant que développeur Python… J’ai mal. Je sais qu’il faut le rendre accessible et que c’est difficile sur 2 pages d’avoir toute la démarche, aussi, je vais vous montrer ici un usage pratique des Comprehension Lists.

Le concept

Commençons par expliquer le concept. Une explosion, c’est un ensemble de particules qui se dispersent à partir d’un point. Une particule est un pixel qui a des coordonnées et une vitesse dans une direction. Elle a également un âge car elle a une durée de vie de 3 secondes. Elle est représentée dans le code par le tuple suivant :

particle = (x, y, vx, vy, age)

Les éléments du tuple représentent :

  • x : abscisse de la particule
  • y : ordonnée de la particule
  • vx : vélocité de la particule sur l’axe des abscisses
  • vy : vélocité de la particule sur l’axe des ordonnées
  • age : l’âge de la particule en millisecondes

Le code du Wireframe Mag

PyGame Zero impose une fonction update(dt) qui est appelée pour générer l’image suivante du jeu. C’est dans cette fonction que doivent être fait tous les calculs pour placer les composants pour l’image suivante. Voici le contenu de cette fonction dans le code original :

# This function updates the array of particles
def update(dt):
    # to update the particle array, create a new empty array
    new_particles = []
    
    # loop through the existing particle array
    for (x, y, vx, vy, age) in particles:
    
        # if a particle was created more than a certain time ago, it can be removed
        if age + dt > MAX_AGE:
            continue
            
        # update the particle's velocity - they slow down over time
        drag = DRAG ** dt
        vx *= drag
        vy *= drag
        
        # update the particle's position according to its velocity
        x += vx * dt
        y += vy * dt
        
        # update the particle's age
        age += dt
        
        # add the particle's new position, velocity and age to the new array
        new_particles.append((x, y, vx, vy, age))
        
    # replace the current array with the new one
    particles[:] = new_particles

Déjà, je n’aime pas du tout le contenu de cette fonction car dans cette boucle, il y a 13 lignes pour mettre à jour les particules. Il faut replacer dans le contexte : la fonction update() est appelée pour mettre à jour tout l’affichage : les particules, les vaisseaux, les astéroïdes, l’arrière-plan… Lorsque nous allons rajouter tous ces composants, comment s’y retrouver ?

Extraire une fonction

Il est donc plus judicieux d’utiliser une fonction pour grouper la mise à jour de ce composant :

def update_particle_data(x, y, vx, vy, age, delay):
    drag = DRAG ** delay
    vx *= drag
    vy *= drag

    x += vx * delay
    y += vy * delay

    age += delay

    return x, y, vx, vy, age

Et le code de la fonction update() devient (j’ai supprimé les commentaires pour une meilleur lisibilité) :

def update(dt):

    new_particles = []
    
    for x, y, vx, vy, age in particles:
    
        if age + dt > MAX_AGE:
            continue
        
        new_particles.append(update_particle_data(x, y, vx, vy, age, dt))
        
    particles[:] = new_particles

Il reste encore 7 lignes destinées à la mise à jour des particules. En toute logique, nous allons déplacer ce code dans une fonction dédiée aux particules. Mais auparavant, il y a une autre réflexion à avoir. Que font ces lignes ?

Comprendre la transformation

Avec les lignes 3 et 15, vous voyez que nous remplaçons la liste de particules par une nouvelle liste. La ligne 10 nous montre que c’est une liste de particules et chaque particule est en fait une particule mise à jour. N’est ce pas finalement une transformation de la liste originale ?

Allons un peu plus loin, ligne 7 et 8, nous voyons que certains éléments ne seront pas traités et donc éliminés selon le critère de la conditionnelle. N’est ce pas un filtre de la liste originale ?

Si vous pouvez mettre en évidence que nous avez un filtre et une transformation de votre liste, alors vous savez que vous devez utiliser les compréhensions lists. Pour la transformation, le code devient :

[update_particle_data(x, y, vx, vy, age, dt)
 for x, y, vx, vy, age in particles]

Et l’ajout du filtre se fait très simplement de la manière suivante :

[update_particle_data(x, y, vx, vy, age, dt)
 for x, y, vx, vy, age in particles
 if age + dt <= MAX_AGE]

Et la fonction update devient :

def update(dt):

    particles[:] = [update_particle_data(x, y, vx, vy, age, dt)
                    for x, y, vx, vy, age in particles
                    if age + dt <= MAX_AGE]

Les particules sont gérées dans une liste. Mettre à jour les informations de chaque particule en fonction de sa vélocité est une transformation de la liste. Supprimer les plus anciennes est un filtre de la liste. Transformation et filtre, c’est ce à quoi servent les Comprehension Lists.

En conclusion

La complexité de cette transformation, le calcul de la mise à jour des coordonnées, est déportée dans une fonction dédiée. Grâce à cette fonction, on met à jour la liste en utilisant les comprehension lists et leur capacité de transformer et filtrer les données de la liste initiale.

Nous avons ainsi un code qui est plus lisible car chaque traitement est à un endroit plus spécifique. C’est ainsi un code qui est plus facile à maintenir car nous n’avons plus à chercher où est traité quelle information. Enfin, nous allons pouvoir avoir une meilleur confiance dans ce code car en déportant la transformation dans une fonction, nous allons pouvoir tester son bon fonctionnement.

Pour aller un peu plus loin…

Il existe une autre manière d’écrire ce code à l’aide de l’opérateur de unpacking (déballage). Pour la transformation, c’est :

[update_particle_data(*particle, dt) for particle in particles]

Mais si nous ajoutons le filtre, il faudra aller chercher l’élément par indice :

[update_particle_data(*particle, dt)
 for particle in particles
 if particle[-1] + dt <= MAX_AGE]

Je vous laisse juger de la pertinence. Pour moi, la transformation est intéressante mais avec le filtre, non.

Si vous avez aimé ce post, n’hésitez pas à laisser un commentaire ci-dessous ou sur la page Facebook 😉

À propos de... Darko Stankovski

iT guy, photographe et papa 3.0, je vous fais partager mon expérience et découvertes dans ces domaines. Vous pouvez me suivre sur les liens ci-dessous.

Vous aimerez aussi...

Laisser un commentaire

En naviguant sur Dad 3.0, vous acceptez l’utilisation de cookies pour une navigation optimale et nous permettre de réaliser des statistiques de visites. Plus d'informations

Le blog Dad 3.0 utilise les cookies pour vous permettre une navigation optimale et nous permettre de réaliser des statistiques de visite. Dad 3.0 affichant des publicités, celles-si utilisent également des cookies pour un ciblage publicitaire. En continuant la navigation sur Dad 3.0, vous acceptez le dépôt et la lecture de cookies.

Fermer