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 révolutionnaires (à l’époque) d’un vieux jeu sasféplu : Defender. Le code complet 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. L’idée sera ici de vous montrer l’étape suivante, celle qui permet de structurer le code.

La première modification consistera à structurer le code grâce aux fonctions. Je pense qu’il est indispensable de connaitre ceci assez tôt dans l’apprentissage. Ensuite, il s’agira d’utiliser une structure un peu particulière de Python, les 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 (N-uplet) 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 réalisés 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 12, vous voyez que nous remplaçons le contenu de la liste de particules par une nouveaux éléments. C’est une manière de remplacer la liste globale sans remplacer la 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() n’a plus besoin de toutes les lignes et 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 dans un composant 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…

Cette réorganisation met en évidence qu’il peut aussi être nécessaire de repenser la manière de transférer l’information. La fonction attends en effet les éléments de la particule, ce qui fait en premier lieu beaucoup de paramètres. Ces données sont gérées par un tuple que nous déballons (unpack) dans la comprehension list.

Et si nous gardions la notion de particule ? Commençons par ne traiter que cette notion dans une transformation.

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

Mais si nous devons filtrer les données par leur âge, il faudra aller chercher l’élément par son indice :

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

Et il nous reste à adapter la fonction qui ne prends plus qu’un paramètre. Cela nécessitera aussi de déballer le tuple dans la fonction.

def update_particle_data(particle, delay):
    x, y, vx, vy, age = particle

    drag = DRAG ** delay
    vx *= drag
    vy *= drag

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

    age += delay

    return x, y, vx, vy, age

L’idée ici est de faciliter la lisibilité en nommant, et donc identifiant, les éléments manipulés.

Je vous laisse juger de ceci dans le code complet disponible sur mon GitHub.

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

2 réponses

  1. 11 avril 2023

    […] vais prendre pour illustration l’article sur les explosions dans les jeux vidéos où une explosion est un ensemble de particules. et où une particule consiste en une coordonnée […]

  2. 20 avril 2023

    […] est infime pour une donnée mais j’ai repris ici le cas des données de l’explosion de l’article des explosions dans les jeux vidéos. Une explosion, c’est 100 particules donc 100 collections de ce type. Et vous pouvez […]

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