1. Hit the fly (shooter)

Dans cette introduction, nous allons développer un premier jeu simple étape par étape. Le jeu consiste en un personnage pouvant se déplacer librement au clavier et pouvant tirer des projectiles sur des ennemis se déplaçant aléatoirement avec des clics de souris.

../_images/hit_the_fly.gif

1. Faire apparaître une fenêtre

La première étape est de définir le nom de notre jeu dans la variable TITLE, ainsi que la largeur WIDTH et la hauteur HEIGHT de notre fenêtre. Nous allons également importer des fonctions de pgzhelper afin de nous faciliter la vie. Pygame Zero s’occupe du reste !

from pgzhelper import *

TITLE = 'Hit the fly'

WIDTH = 800
HEIGHT = 600
../_images/pygame_fenetre.png

Pygame Zero fonctionne avec 2 fonctions principales: draw et update. Tandis que draw est appelée pour afficher des choses à l’écran, update est appelée pour faire évoluer le jeu. Elles sont toutes 2 appelées en boucle automatiquement tant qu’on ne quitte pas le jeu.

from pgzhelper import *

TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600

def draw():
    pass # Ajouter ici tout ce qui concerne l'affichage

def update():
    pass # Ajouter ici tout ce qui concerne l'évolution du jeu

2. S’occuper du fond d’écran

Tous les dessins de notre jeu se feront via la fonction draw de Pygame. Il nous faut donc lui ajouter l’instruction permettant de colorier le fond.

def draw():
    screen.fill('red') # Colorie le fond en rouge

screen est un objet accessible grâce à Pygame qui représente la fenêtre de notre jeu.
Bon c’est joli mais ça serait sympa d’avoir une image de fond plutôt qu’une simple couleur. Prenons simplement de l’herbe.

../_images/grass.png

Voici comment faire:

def draw():
    screen.blit('grass', (0, 0)) # Dessine l'image 'grass' aux coordonnées (0, 0)

La méthode blit(image, (x, y)) de screen permet de dessiner une image sur la fenêtre aux coordonnées (x, y) correspondant au coin supérieur gauche de l’image. Comme l’image est au moins aussi grande que la fenêtre, elle la recouvre entièrement.

3. Ajouter le joueur

Il est temps d’ajouter notre joueur. Il sera représenté par un objet. Voyez un objet comme une sorte de super variable qui peut contenir d’autres variables. Chaque objet sera également représenté par une image que nous appelerons un sprite. Chaque sprite doit être sauvegardé dans le dossier images.

../_images/alien_walk1.png

La plateforme Kenny contient énormément de sprites gratuits à utiliser pour vos jeux. Le sprite du petit alien ci-dessus vient du Platformer art deluxe.

Pour chaque nouvel objet que l’on veut ajouter à notre jeu, nous allons créer une classe associée. Voyez une classe comme un moule qui nous permettra de créer des objets, comme notre joueur. En programmation orientée objet, une classe possède des attributs (des variables décrivant l’objet) et des méthodes (des fonctions pouvant être appelées par l’objet).

../_images/classe_voiture.svg

Nous créons donc une classe Player qui hérite des attributs et méthodes d’une autre classe nommée Actor offerte par Pygame. En fait, tous les objets que nous allons ajouter à notre jeu sont des acteurs.

class Player(Actor):
    def __init__(self, image, pos, **kwargs): # Init est le constructeur de la classe
        super().__init__(image, pos, **kwargs) # Cette ligne appelle le constructeur de la classe mère Actor
        # Ne vous en préoccupez pas trop pour le moment

Ce code crée une classe Player héritant de la classe Actor. La méthode init est ce que l’on appelle un constructeur qui est appelé lors de la création d’un objet. Ce construction prend obligatoirement une image en entrée, ainsi que des coordonnées pos pour savoir où placer l’objet créé.

Nous pouvons à présent créer notre objet player grâce à notre classe. Nous lui passons le nom de l’image qui va le représenter ainsi que ses coordonnées (x, y) afin qu’il soit placé au centre de la fenêtre.

player = Player('alien_walk1', (WIDTH/2, HEIGHT/2)) # Appel du constructeur init

Afin de dessiner notre joueur, il faut en dernier lieu appeler la méthode player.draw() dans la fonction principale draw de Pygame.

def draw():
    screen.blit('grass', (0, 0))
    player.draw() # On dessine le joueur ici !

draw est une méthode possèdée par tous les objets de type Actor. Etant donné que notre objet de type Player est aussi un Actor, il hérite de cette méthode ainsi que de plein d’attributs dont nous aurons besoin plus tard comme les coordonnées x et y de notre objet.

4. Déplacer le joueur

Nous allons déplacer le joueur grâce aux touches w, a, s, d. Une manière propre de le faire est de définir une méthode update pour notre classe Player qui va définir comment notre joueur doit se comporter.

class Player(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
    
    def update(self):
        # Que doit faire notre joueur...

Remarquez que, tout comme init, update prend un argument spécial: self. Cet argument self représente l’objet en lui-même (c’est à dire le joueur dans notre cas). C’est grâce à lui que nous pourrons modifier par exemple la position de notre joueur via self.x qui correspond à sa position horizontale.

keyboard est (tout comme screen) un objet donné par Pygame qui nous permet de récupérer les touches enfoncées par l’utilisateur. C’est très simple d’utilisation: si la touche x est enfoncée, alors keyboard.x vaudra True, sinon il vaudra False.

if keyboard.a: # Si la touche a est appuyée
    # Déplace le joueur à gauche

Il nous suffit donc de contrôler quelles touches sont enfoncées et de modifier les coordonnées de notre joueur en conséquence:

class Player(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
    
    def update(self):
        if keyboard.a:
            self.x -= 3 # Equivalent à self.x = self.x - 3
        elif keyboard.d:
            self.x += 3 # Equivalent à self.x = self.x + 3

        if keyboard.w:
            self.y -= 3
        elif keyboard.s:
            self.y += 3

Vous remarquez que nous bougeons de 3 pixels par tick (un tick représente un tour dans la boucle principale du jeu). Afin de rendre le code plus propre et pouvoir facilement modifier la vitesse de notre personnage par la suite, il serait plus judicieux de lui définir une vitesse.

Nous pouvons ajouter un attribut speed à notre classe Player et lui donner la valeur 3. Ainsi, nous n’aurons ensuite plus qu’à modifier cette valeur pour accélérer ou ralentir notre personnage à notre guise. Cet ajout se fait dans le constructeur de notre classe, c’est à dire dans la méthode init.

class Player(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.speed = 3 # Ajout de l'attribut ici !
    
    def update(self):
        if keyboard.a:
            self.x -= self.speed
        elif keyboard.d:
            self.x += self.speed

        if keyboard.w:
            self.y -= self.speed
        elif keyboard.s:
            self.y += self.speed

Afin que le jeu comprenne à présent qu’il doit effectuer l’update de notre joueur, il nous faut encore appeler la méthode player.update() dans la fonction update principale du jeu:

def update():
    player.update()

Dernière étape: faire en sorte que le sprite représentant le joueur s’adapte à sa direction. Par défaut il regarde à droite, mais on voudrait qu’il regarde à gauche s’il se déplace à gauche.

Ceci est faisable grâce à l’attribut flip_x hérité de la classe Actor. Par défaut, flip_x vaut False. Il suffit de le faire passer à True au bon moment.

class Player(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.speed = 3
    
    def update(self):
        if keyboard.a:
            self.x -= self.speed
            self.flip_x = True # On regarde à gauche, on flipe
        elif keyboard.d:
            self.x += self.speed
            self.flip_x = False #On regarde à droite, on ne flipe pas

        if keyboard.w:
            self.y -= self.speed
        elif keyboard.s:
            self.y += self.speed

5. Empêcher un acteur de sortir de l’écran

La détection des bords de la fenêtre de jeu est un élément récurrent de tout jeu vidéo. Il existe plétore de manières de résoudre ce problème mais je vous propose la solution suivante: si un acteur sort de l’écran, il réapparaît du coté opposé.

Comme cela ne concernera pas que notre joueur mais probablement d’autres acteurs de notre jeu, il semble être une bonne idée de créer une fonction qui pourra s’appliquer à n’importe quel acteur. Notre fonction detect_border prendra donc un acteur en argument.

def detect_border(actor):
    # Ajouter ici la détection des bords
    # et la modification des coordonnées de l'acteur si nécessaire

Tout Actor possède des attributs x et y que nous pouvons lire pour savoir si l’acteur sort de la fenêtre de jeu. Par exemple, si actor.x > WIDTH, cela signifie que l’acteur sort du jeu par la droite. Dans ce cas, vous voulons faire réapparaître l’acteur à gauche, c’est à dire que actor.x doit valoir 0.

Nous n’avons qu’à faire les 4 tests pour les 4 bords de la fenêtre:

def detect_border(actor):
    if actor.x > WIDTH: # Dépassement à droite
        actor.x = 0
    elif actor.x < 0: # Dépassement à gauche
        actor.x = WIDTH

    if actor.y > HEIGHT: # Dépassement en bas
        actor.y = 0
    elif actor.y < 0:
        actor.y = HEIGHT # Dépassement en haut

Mais à quel moment appeler cette fonction ? Eh bien à la fin du déplacement de notre personnage ! C’est à dire à la fin de la méthode player.update.

class Player(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.speed = 3

    def update(self):
        if keyboard.a:
            self.x -= self.speed
            self.flip_x = True
        elif keyboard.d:
            self.x += self.speed
            self.flip_x = False

        if keyboard.w:
            self.y -= self.speed
        elif keyboard.s:
            self.y += self.speed
            
        detect_border(self) # Appel de la fonction ici !

Notez ici que l’argument donné à detect_border est self car il représente justement le joueur !

6. Ajouter un ennemi

Il est temps d’ajouter un ennemi pour pimenter un peu notre jeu. Dans notre cas, ce sera une mouche virevoltant aléatoirement.

../_images/fly1.png

Comme pour Player, la première étape est de créer une nouvelle classe pour représenter notre ennemi.

class Ennemy(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
    
    def update(self):
        pass # Ajouter ici les instructions codant le comportant de l'ennemi

Nous pouvons ensuite créer un ennemi en appelant son constructeur et en lui donnant un sprite et des coordonnées de départ aléatoires. La foncton permettant de tirer un nombre entier aléatoire entre 2 bornes a et b se nomme randint(a, b). Pour qu’elle fonctionne, nous devons également importer le module random qui contient cette fonction.

from random import * # Importe tout un tas de fonctions pour faire de l'aléatoire

# reste du code...

player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT))) # Création de l'ennemi aux coordonnées aléatoires sur la fenêtre

N’oublions pas d’appeler la méthode ennemy.draw() dans la fonction draw du programme principal. Nous pouvons également déjà appeler la méthode ennemy.update() dans la fonction update même si elle ne fait rien pour l’instant.

def draw():
    screen.blit('grass', (0, 0))
    player.draw()
    ennemy.draw() # On dessine l'ennemi !

def update():
    player.update()
    ennemy.update() # Ne fait rien pour le moment

Occupons nous à présent du déplacement de l’ennemi. Cela se code dans la méthode update de la classe Ennemy. Nous allons utiliser l’attribut direction qui indique la direction dans laquelle notre ennemi va se déplacer. Il s’agit d’un angle entre 0 et 360 degrés où 0 correspond à un déplacement vers la droite.

Nous allons donc spécifier la valeur de cet attribut lors de la création de notre objet, dans la méthode init. Nous en profitons pour également définir sa vitesse (speed) à 3.

class Ennemy(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = randint(0, 360) # Direction initiale aléatoire
        self.speed = 3 # Vitesse de déplacement

    def update(self):
        pass

Ecrivons à présent la méthode update qui va définir le comportement de notre ennemi à chaque tick. Nous souhaitons faire 3 choses:

  1. Légèrement modifier la direction de manière aléatoire

  2. Avancer dans la direction (la méthode move_in_direction(speed) héritée de la classe Actor permet de le faire)

  3. Détecter les bords de la fenêtre

class Ennemy(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = randint(0, 360) # Direction initiale aléatoire
        self.speed = 3 # Vitesse de déplacement

    def update(self):
        self.direction += randint(-10, 10) # Modification aléatoire de la direction
        self.move_in_direction(self.speed) # Avancer dans la direction
        detect_border(self) # Détecter les bords comme pour le joueur

7. Animer un acteur

Nous allons tenter d’ajouter un peu de vie à notre jeu en animant notre ennemi.
Pour animer un objet, il faut au minimum 2 sprites qui vont se succéder à une certaine vitesse (fps).

../_images/fly1.png ../_images/fly2.png

Tous nos objets héritant de la classe Actor possèdent un attribut nommé images qui permet de stocker la liste des images de l’animation.
La première étape consiste donc à définir la valeur de cet attribut images dans le constructeur de notre ennemi.

class Ennemy(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.images = ['fly1', 'fly2'] # Nouvel attribut ici !
        self.direction = randint(0, 360)
        self.speed = 3
    #...

Par la suite, tout ce qu’il reste à faire est appeler la méthode ennemy.animate() dans la fonction draw du programme principal. Cette méthode héritée de Actor permet de faire avancer l’animation de notre objet.

def draw():
    screen.blit('grass', (0, 0))
    player.draw()
    ennemy.draw()
    ennemy.animate() # On fait avancer l'animation ici !

Notez que par défaut, l’animation se déroule à 5 fps. Cela signifie que l’image change 5 fois par seconde. Pour changer cette valeur, il suffirait de modifier l’attribut fps dans le constructeur de la classe Ennemy.

8. Ajouter des ennemis à intervalle régulier

Pour avoir plusieurs ennemis en même temps dans le jeu, nous allons remplir une liste d’ennemis. Nous allons donc remplacer notre unique objet ennemy par une liste ([]) qui contient initialement un seul ennemi.

ennemies = [Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))]

Evidemment, il faut mettre à jour les fonctions principales draw et update car au lieu de desinner ennemy, nous voulons à présent parcourir la liste ennemies et dessiner chacun d’eux.

def draw():
    screen.blit('grass', (0, 0))
    player.draw()
    for ennemy in ennemies: # Parcourt la liste des ennemis
        ennemy.draw() # Dessine chacun d'eux
        ennemy.animate() # Avance l'animation de chacun d'eux

def update():
    player.update()
    for ennemy in ennemies: # Parcourt la liste des ennemis
        ennemy.update() # Met à jour chacun d'eux

Nous avons à présent une liste d’ennemis qui contient 1 ennemi. Mais comment remplir cette liste au cours du jeu ? Commençons par définir une fonction qui va s’occuper d’ajouter un nouvel ennemi à la liste.

def add_ennemy():
    ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
    ennemies.append(ennemy) # Ajoute ennemy à la liste ennemies

La question est: quand appeler cette fonction ? Quand voulons nous ajouter un ennemi ? Ici, disons que nous aimerions ajouter un ennemi toutes les 15 secondes.

Pygame nous offre pour ceci un objet très utile: clock. Cet objet permet d’agender des appels de fonction dans le temps.

clock.schedule_interval(add_ennemy, 15.0)

La méthode schedule_interval permet d’appeler une fonction toutes les x secondes. Nous l’avons donc réglée pour appeler add_ennemy toutes les 15 secondes.

9. Tirer des projectiles

Il est temps de pouvoir frapper nos ennemis ! Nous allons créer un nouvel objet pour représenter un projectile qui sera tiré avec un clic de souris et qui partira depuis le joueur dans la direction du curseur de la souris.

../_images/missile.png

Commençons par créer la classe Missile associée. Chaque missile créé aura une direction de départ différente, cela signifie que nous devons donner cette information dès sa création. Nous ajoutons donc un argument direction à son constructeur init.
Nous pouvons également déjà lui donner une vitesse (de 5 par exemple).

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction # La direction initiale dépendra de la valeur donnée lors de la création du missile
        self.speed = 5

    def update(self):
        pass # On s'en occupe plus tard

Créons à présent une liste de missiles, tout comme pour les ennemis. Cette liste doit être vide au départ et se remplira à chaque clic de souris.

player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
ennemies = [Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))]
missiles = [] # Création de la liste vide de missiles

Nous voulons ajouter un missile à chaque clic de souris. Pygame nous fournit la fonction on_mouse_down(pos) qui nous permet de réagir à un clic de souris. pos correspond à l’emplacement (x, y) du curseur au moment du clic.

Nous devons utiliser pos pour calculer la direction que devra prendre notre missile. Il est possible de calculer l’angle entre un acteur et une coordonnée grâce à la méthode angle_to héritée de Actor.

def on_mouse_down(pos):
    direction = player.angle_to(pos) # Calcul de l'angle entre le joueur et le curseur de la souris
    missile = Missile('missile', (player.x, player.y), direction) # Création du missile avec la direction calculée
    missiles.append(missile) # Ajout du missile à la liste

Dessinons à présent nos missiles en les ajoutant à la fonction draw. On peut déjà l’ajouter également à update même si on a pas encore défini les déplacements du missile.

def draw():
    screen.blit('grass', (0, 0))
    player.draw()
    for ennemy in ennemies:
        ennemy.draw()
        ennemy.animate()
    for missile in missiles: # Parcourt les missiles
        missile.draw()

def update():
    player.update()
    for ennemy in ennemies:
        ennemy.update()
    for missile in missiles: # Parcourt les missiles
        missile.update()

Il ne nous reste plus qu’à définir le déplacement de nos missiles dans leur méthode update. C’est assez simple, nous voulons simplement qu’il avance dans sa direction avec sa vitesse.

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction
        self.speed = 5

    def update(self):
        self.move_in_direction(self.speed)

Vous remarquez un dernier détail visuel à régler… Les sprites des missiles devraient s’orienter dans la direction de leur déplacement. Chaque Actor possède un attribut angle qui règle l’angle du sprite associé. Par défaut, cet angle vaut 0.

../_images/rotation.svg

Il suffit donc de donner à angle la même valeur que direction à la création du missile.

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction
        self.angle = direction # Ajout de l'attribut ici !
        self.speed = 5

    def update(self):
        self.move_in_direction(self.speed)

10. Gérer les collisions

Il est temps de donner du pouvoir à nos missiles pour détruire les ennemis ! Normalement, la gestion des collisions est un moment compliqué dans le développement d’un jeu car cela requiert des algorithmes assez complexes. Heureusement pour nous, Pygame nous mâche le travail grâce à la classe Actor qui est déjà capable de détecter des collisions grâce à sa méthode collides_with(other_actor). Elle retourne True si l’acteur est en contact avec other_actor.

../_images/collision.png

Où ajouter le contrôle de collision ? Eh bien dans la méthode update du missile ! Après le déplacement, on parcourt la liste des ennemis et on contrôle si le missile est en contacte avec l’un d’eux.

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction
        self.angle = direction
        self.speed = 5

    def update(self):
        self.move_in_direction(self.speed)
        for ennemy in ennemies: # Parcourt chaque ennemi
            if self.collides_with(ennemy): # Si il y a collision
                print('BOOOOM') # On affiche boom dans la console

Bon, on affiche BOOOOM quand on touche en ennemi, mais on voudrait bien qu’il soit détruit, ainsi que notre missile ! Pour détruire un acteur du jeu, il suffit de le supprimer de la liste qui le contient. C’est à dire que nous voulons supprimer le missile de missiles et l’ennemi touché de ennemies.

Mais à quel moment ? En fait il existe plein de manières de faire… Une façon propre est d’utiliser l’attribut to_remove qui traque si un objet doit être retiré du jeu ou non (il vaut False par défaut). Il suffit donc de passer to_remove à True pour le missile en question et pour l’ennemi qu’il a touché.

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction
        self.angle = direction
        self.speed = 5

    def update(self):
        self.move_in_direction(self.speed)
        for ennemy in ennemies:
            if self.collides_with(ennemy):
                print('BOOOOM')
                ennemy.to_remove = True # On marque l'ennemi touché
                self.to_remove = True # On marque le missile en question

A la fin de la fonction update du programme principal, on parcourt nos listes et on supprime tous les objets qui sont marqués pour être détruits. Cela permet de tous les détruire en même temps pour éviter les bugs et simplifier la logique du jeu.
Par soucis de simplicité, j’ai écrit une fonction toute prête: remove_actors(actors) qui s’occupe de supprimer de la liste actors tous les acteurs dont l’attribut to_remove est à True.

def update():
    player.update()
    for ennemy in ennemies:
        ennemy.update()
    for missile in missiles:
        missile.update()
        
    remove_actors(ennemies) # On supprime les ennemis marqués
    remove_actors(missiles) # On supprime les missiles marqués

11. Ajouter un son de collision

Ajouter un bruitage est très simple. La première étape consiste à ajouter le fichier .wav souhaité dans le dossier sounds. De nombreux bruitages et musiques gratuits peuvent être trouvés sur OpenGameArt. Priviliégiez les sons courts pour éviter des problèmes de performance.

Pygame nous offre l’objet sounds qui nous permet de facilement lancer un bruitage. Dans notre cas, nous souhaitons lancer un bruit d’explosion lorsque qu’un missile touche un ennemi.

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction
        self.angle = direction
        self.speed = 5

    def update(self):
        self.move_in_direction(self.speed)
        for ennemy in ennemies:
            if self.collides_with(ennemy):
                print('BOOOOM')
                sounds.explosion.play() # Ajout du bruitage ici !
                ennemy.to_remove = True
                self.to_remove = True

sounds.explosion.play() fait ici référence au nom du fichier son: explosion.wav.

12. Ajouter une musique de fond

Ajouter une musique de fond est tout aussi simple ! Il suffit d’ajouter un fichier .mp3 dans le dossier music et d’utiliser l’objet music offert par Pygame.

Pour lancer notre musique adventure.mp3 au début du jeu, il reste qu’à appeler music.play(adventure) au début du programme principal.

music.play('adventure') # Ajout de la musique ici !

player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
ennemies = [Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))]
missiles = []

clock.schedule_interval(add_ennemy, 15.0)

13. Tenir et afficher un score

Le score est important dans les jeux ! Pour en tenir un, on peut le définir dans les attributs de notre player et l’initialiser à 0.

class Player(Actor):
    def __init__(self, image, pos, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.speed = 3
        self.score = 0 # Création de l'attribut score ici !

Avant de nous occuper de l’augmentation du score, voyons déjà comment l’afficher. Afficher du texte à l’écran se fait via l’objet screen offert par Pygame. Nous l’utilisons dans la fonction draw du programme principal, juste après avoir défini l’image de fond.

def draw():
    screen.blit('grass', (0, 0))
    screen.draw.text(f"Score: {player.score}", (0, 0)) # Ajout du score ici aux coordonnées (0, 0), tout en haut à gauche
    player.draw()
    for ennemy in ennemies:
        ennemy.draw()
        ennemy.animate()
    for missile in missiles:
        missile.draw()

Le score va augmenter à chaque fois que l’on détruit un ennemi. Il suffit donc de l’augmenter de 1 à chaque fois qu’un missile détruit un missile dans sa méthode update.

class Missile(Actor):
    def __init__(self, image, pos, direction, **kwargs):
        super().__init__(image, pos, **kwargs)
        self.direction = direction
        self.angle = direction
        self.speed = 5

    def update(self):
        self.move_in_direction(self.speed)
        for ennemy in ennemies:
            if self.collides_with(ennemy):
                print('BOOOOM')
                sounds.explosion.play()
                ennemy.to_remove = True
                self.to_remove = True
                player.score += 1 # Augmentation du score de 1

14. Idées d’améliorations

Voici plusieurs idées d’amélioration du jeu.
Vous pouvez bien sûr me proposer d’autres idées et je vous dirai leur difficulté.

Totalement dans vos cordes (facile):

  • Ajouter une animation au joueur quand il se déplace.

  • Faire en sorte que le jeu arrête de créer des ennemis s’il y en a déjà 5 ou plus en jeu.

  • Faire en sorte que plus le score augmente, plus les ennemis se déplacent rapidement.

  • Détruire les missiles lorsqu’ils sortent de l’écran et limiter le joueur à 3 missiles à la fois.

  • Faire en sorte que le joueur perde de la vie à chaque contact avec un ennemi.

  • Créer un game over sous les conditions de votre choix (par exemple au contact d’un ennemi ou après un certain temps). Cela affiche par exemple un écran noir avec écrit “Game Over” en grand.

  • Faire en sorte que le personnage ne se déplace pas plus rapidement en diagonal qu’à l’horizontal ou à la verticale.

  • Permettre de mettre le jeu en pause avec la touche espace.

Un peu plus complexe (moyen):

  • Faire en sorte que les ennemis suivent le joueur au lieu de se balader aléatoirement.

  • Faire en sorte qu’en cas de contact avec un ennemi, le joueur perd de la vie et l’ennemi meurt. La vie du joueur est affichée sous le score.

  • Permettre au joueur de ramasser des objets pour augmenter son score, par exemple des pièces.

  • Ajouter un nouveau type d’ennemi qui se comporte différemment.

  • Ajouter un item qui permet d’augmenter temporairement la vitesse du joueur si rammassé.

  • Faire en sorte qu’en cas de contact avec un ennemi, le joueur perde un peu de vie et soit invincible pendant un court laps de temps (pour éviter des dégats à répétition).

  • Ajouter un boss qui apparaît seulement après un certain temps et nécessite beaucoup de dégats pour être battu mais offre un score élevé.

  • Ajouter un second joueur qui pourra se déplacer et tirer avec d’autres touches.

Challenging (difficile):

  • Ajouter des armes au personnage. Par exemple une attaque au corps à corps ou la possibilité de larguer des bombes qui explosent après quelques secondes.

  • Permettre au joueur de poser des pièges qui blesseront les ennemis qui passent dessus.

  • Faire en sorte que le monde bouge autour du personnage afin qu’il puisse se balader librement dans toutes les directions sans jamais toucher un bord. Indice: le joueur ne se déplace plus, mais déplacez tous les acteurs du jeu dans la direction inverse au déplacement du joueur.

  • Créer un second niveau atteignable en passant un certain score où les ennemis sont différents et plus coriaces.

  • Donner aux ennemis la capacité d’essayer d’esquiver les projectiles (en se déplaçant perpendiculairement à leur trajectoire).

  • Créer un menu avant le début du jeu qui permet de choisir la difficulté ou d’autres options.