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.

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

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.

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.
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
def draw():
screen.blit('grass', (0, 0))
def update():
pass
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
.

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).
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.
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
class Player(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
def draw():
screen.blit('grass', (0, 0))
player.draw()
def update():
pass
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
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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
player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
def draw():
screen.blit('grass', (0, 0))
player.draw()
def update():
player.update()
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 !
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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)
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
def draw():
screen.blit('grass', (0, 0))
player.draw()
def update():
player.update()
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.

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:
Légèrement modifier la direction de manière aléatoire
Avancer dans la direction (la méthode
move_in_direction(speed)
héritée de la classeActor
permet de le faire)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
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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)
class Ennemy(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.direction = randint(0, 360)
self.speed = 3
def update(self):
self.direction += randint(-10, 10)
self.move_in_direction(self.speed)
detect_border(self)
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
def draw():
screen.blit('grass', (0, 0))
player.draw()
ennemy.draw()
def update():
player.update()
ennemy.update()
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).


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
.
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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)
class Ennemy(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.images = ['fly1', 'fly2']
self.direction = randint(0, 360)
self.speed = 3
def update(self):
self.direction += randint(-10, 10)
self.move_in_direction(self.speed)
detect_border(self)
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
def draw():
screen.blit('grass', (0, 0))
player.draw()
ennemy.draw()
ennemy.animate()
def update():
player.update()
ennemy.update()
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.
Voir le code complet à ce point
from pgzhelper import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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)
class Ennemy(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.images = ['fly1', 'fly2']
self.direction = randint(0, 360)
self.speed = 3
def update(self):
self.direction += randint(-10, 10)
self.move_in_direction(self.speed)
detect_border(self)
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
def add_ennemy():
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
ennemies.append(ennemy)
player = Player('alien_walk1', (WIDTH/2, HEIGHT/2))
ennemies = [Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))]
clock.schedule_interval(add_ennemy, 15.0)
def draw():
screen.blit('grass', (0, 0))
player.draw()
for ennemy in ennemies:
ennemy.draw()
ennemy.animate()
def update():
player.update()
for ennemy in ennemies:
ennemy.update()
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.

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
.
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)
Voir le code complet à ce point
from pgzhelper import *
from random import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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)
class Ennemy(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.images = ['fly1', 'fly2']
self.direction = randint(0, 360)
self.speed = 3
def update(self):
self.direction += randint(-10, 10)
self.move_in_direction(self.speed)
detect_border(self)
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)
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
def add_ennemy():
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
ennemies.append(ennemy)
def on_mouse_down(pos):
direction = player.angle_to(pos)
missile = Missile('missile', (player.x, player.y), direction)
missiles.append(missile)
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)
def draw():
screen.blit('grass', (0, 0))
player.draw()
for ennemy in ennemies:
ennemy.draw()
ennemy.animate()
for missile in missiles:
missile.draw()
def update():
player.update()
for ennemy in ennemies:
ennemy.update()
for missile in missiles:
missile.update()
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
.

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
Voir le code complet à ce point
from pgzhelper import *
from random import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
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)
class Ennemy(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.images = ['fly1', 'fly2']
self.direction = randint(0, 360)
self.speed = 3
def update(self):
self.direction += randint(-10, 10)
self.move_in_direction(self.speed)
detect_border(self)
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)
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
def add_ennemy():
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
ennemies.append(ennemy)
def on_mouse_down(pos):
direction = player.angle_to(pos)
missile = Missile('missile', (player.x, player.y), direction)
missiles.append(missile)
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)
def draw():
screen.blit('grass', (0, 0))
player.draw()
for ennemy in ennemies:
ennemy.draw()
ennemy.animate()
for missile in missiles:
missile.draw()
def update():
player.update()
for ennemy in ennemies:
ennemy.update()
for missile in missiles:
missile.update()
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
Voir le code final
from pgzhelper import *
from random import *
TITLE = 'Hit the fly'
WIDTH = 800
HEIGHT = 600
class Player(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.speed = 3
self.score = 0
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)
class Ennemy(Actor):
def __init__(self, image, pos, **kwargs):
super().__init__(image, pos, **kwargs)
self.images = ['fly1', 'fly2']
self.direction = randint(0, 360)
self.speed = 3
def update(self):
self.direction += randint(-10, 10)
self.move_in_direction(self.speed)
detect_border(self)
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
def detect_border(actor):
if actor.x > WIDTH:
actor.x = 0
elif actor.x < 0:
actor.x = WIDTH
if actor.y > HEIGHT:
actor.y = 0
elif actor.y < 0:
actor.y = HEIGHT
def add_ennemy():
ennemy = Ennemy('fly1', (randint(0, WIDTH), randint(0, HEIGHT)))
ennemies.append(ennemy)
def on_mouse_down(pos):
direction = player.angle_to(pos)
missile = Missile('missile', (player.x, player.y), direction)
missiles.append(missile)
music.play('adventure')
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)
def draw():
screen.blit('grass', (0, 0))
screen.draw.text(f"Score: {player.score}", (0, 0))
player.draw()
for ennemy in ennemies:
ennemy.draw()
ennemy.animate()
for missile in missiles:
missile.draw()
def update():
player.update()
for ennemy in ennemies:
ennemy.update()
for missile in missiles:
missile.update()
remove_actors(ennemies)
remove_actors(missiles)
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.