Introduction au développement de jeu vidéo
Ce chapitre présente succintement quelques principes de développement de jeu vidéo en temps réel avec la bibliothèque Pygame.
Le temps réel veut dire que les entités du jeu évoluent en continue (ex : Minecraft, CS2, Fortnite, Mario). C'est l'opposé du tour par tour (ex : Dofus).
Principe
Le code d'un niveau de jeu a en général la structure suivante :
L'état du niveau est initialisé au lancement du niveau.
Ensuite, le code du niveau simule l'évolution de l'état par petites intervalles de temps, appelés frames ou ticks:
A chaque frame, le code :
- récupère les éventuelles intéractions faites par le joueur, par exemple un appui sur une touche.
- Simule l'évolution de l'état du niveau sur la durée d'une frame, en prenant en compte les intéractions du joueur.
- Affiche le nouvel état du niveau au joueur.
Installation de pygame et première fenêtre
Commencez par installer Pygame. Si vous n'y arrivez pas, utilisez la méthode dans le dépliant suivant :
Installation de Pygame par un script
Créez un fichier Python, copiez-collez y le script suivant, et executez-le avec votre outils de développement (par exemple, Thonny).
Attention
Il est peu recommandé d'exécuter des scripts qui ne proviennent pas d'une source fiable sans les comprendre, n'en faite pas une habitude !
import subprocess
import sys
def install(package):
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
install("pygame")
Vous devriez obtenir une sortie de cette forme :
Defaulting to user installation because normal site-packages is not writeable
Collecting pygame
Downloading pygame-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (13.7 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13.7/13.7 MB 2.8 MB/s eta 0:00:00
Installing collected packages: pygame
Successfully installed pygame-2.1.3
L'important est la dernière ligne, qui doit mentionner "Successfully installed".
Si vous pygame est déjà installé, vous aurez une sortie de la forme :
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: pygame in ./.local/lib/python3.10/site-packages (2.1.3)
Qui mentionne "Requirement already satisfied : pygame".
Super. Voici le code de départ :
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran
}
def entrees_joueur():
quitter = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
return {
QUITTER : quitter
}
def niveau():
etat = initialiser_niveau()
stop = False
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
niveau()
Si vous exécutez ce code, vous devriez voir apparaître une fenêtre toute noire :

Que vous pouvez quitter en cliquant sur ECHAP.
Félicitation, vous avez créé votre première fenêtre !
Voyons plus en détail ce que fait le code :
import pygame
from pygame import *
# En python, les variables avec un nom en majuscules indiquent qu'elles ne doivent pas être modifiées
# C'est une convention, et non quelque chose que Python force. On les appelle des "constantes".
# On va utiliser des dictonnaires pour stocker toutes nos valeurs et les organiser.
# Et les constantes vont nous servir de clé. Pourquoi ?
# Si on se trompe dans l'écriture d'une constante, Python affichera une erreur, alors qu'avec
# directement une chaîne de caractère, Python ne dira rien en cas d'erreur.
ECRAN = "ECRAN"
QUITTER = "QUITTER"
# Cette fonction initialise la totalité de l'état du niveau. Pour le moment, elle ne fait pas grand chose.
def initialiser_niveau():
# On dit à Pygame de démarrer
pygame.init()
# On crée la fenêtre, de taille 1280x720.
# set_mode retourne une *Surface*, c'est à dire une zone sur laquelle on peut dessiner
ecran = pygame.display.set_mode([1280, 720])
return {
# Pour le moment, le seul état du niveau est... l'écran !
ECRAN : ecran
}
# Cette fonction récupère les intéractions du joueur.
def entrees_joueur():
quitter = False
# pygame.event.get retourne la liste des évènements (touche appuyé, souris bougée, ...)
# qui ont eu lieu depuis le dernier appel à pygame.event.get
for evt in pygame.event.get():
# On regarde le type de l'évènement. Ici, vrai si l'évènement corresponds à un appui sur une touche
if evt.type == pygame.KEYDOWN:
# On regarde sur quelle touche a appuyé le joueur. Ici, vrai si le joueur a appuyé sur ECHAP.
if evt.key == pygame.K_ESCAPE:
quitter = True
# On retourne les entrées sous forme d'un dictionnaire qui récapitule les actions que le joueur
# a effectuées cette frame
return {
QUITTER : echap
}
# Cette fonction contient le code principal du niveau
def niveau():
#création de l'état
etat = initialiser_niveau()
#Boucle infinie de la frame.
stop = False
#Stop sert à quitter si le joueur appuie sur échap
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
# Ferme la fenetre
pygame.quit()
# On appelle la fonction pour lancer le niveau
niveau()
Premier dessin
Pygame nous donne deux manières de réaliser l'affichage :
La première consiste à dessiner des formes à l'écran. La seconde à "coller" des images sur l'écran. Voyons la première.
Ajoutons un personnage et dessinons-le sous forme d'une cercle.
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
#on se crée une fonction pour créer le personnage
# On va toujours passer par des fonction et pas directement par des dicts
# Parce que les fonctions permettent plus de vérifications. On aura l'occasion d'en reparler.
def creer_personnage():
# Vector2 est un type fourni par Pygame, qui représente un ... vecteur en 2 d
# et supporte toutes les opérations de base sur les vecteurs : normalisation, addition, soustraction, multiplication, ...
return {
POSITION : Vector2(0, 0)
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
#on ajoute le personnage au niveau
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage()
}
def entrees_joueur():
quitter = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
return {
QUITTER : quitter
}
# On crée une fonction pour afficher le niveau
def afficher_niveau(etat):
# remplit l'écran de la couleur noire (code RGB 0, 0, 0)
etat[ECRAN].fill((0, 0, 0))
#pygame.draw.circle dessine un cercle sur une surface. Les paramètres sont les suivants :
# - la Surface sur laquelle dessiner (ici, l'écran)
# - Le code RGB de la couleur, (196, 154, 214) corresponds à du rose/violet
# - le centre du cercle. ici, ce sera 0, 0, la position que l'on a donné à notre personnage.
# Elle corresponds à la position en pixels sur l'écran
# - Le rayon du cercle, egalement en pixels
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION],
20
)
# pygame.display.flip permet de valider le dessin et de l'afficher à l'écran.
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
stop = False
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
#bien sûr, on appelle la fonction ici :
afficher_niveau(etat)
pygame.quit()
niveau()
Normalement, en lançant le code, vous verrez apparaître le cercle dans le coin haut gauche de la fenêtre :

En HAUT à gauche ? mais comment ça ? Et bien tout simplement, le repère de l'écran est inversé : l'axe y croît vers le bas.
!!! example "Activité"*
=== "Enoncé, partie 1"
Essayez de changer la position du personnage pour que le cercle apparaîsse en bas à gauche de la fenetre.
=== "Solution"
Il faut changer la position du cercle avec y au maximum.
La fenetre fait 720 pixels de haut, donc :
```python
def creer_personnage():
return {
POSITION : Vector2(0, 720)
}
```
===! "Enoncé, partie 2"
Même chose, mais au milieu de la fenêtre
=== "Solution"
On prends la moitié à chaque fois :
```python
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2)
}
```
Déplacer le personnage
On va rendre le personnage réactif aux entrées de l'utilisateur, en le déplaçant avec les flèches.
On commence par UNE des directions.
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE = "VITESSE"
def creer_personnage():
# On ajoute une vitesse et une direction au personnage
# La direction est un vecteur indiquant vers où le perso va
# la vitesse un nombre qui indique la vitesse du déplacement.
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 10
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage()
}
def entrees_joueur():
quitter = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
# cette fonction renvoie l'état actuel de toutes les
# touches du clavier. Une touche vaut True si elle est
# enfoncée, False sinon
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
# pygame.K_RIGHT est la touche fleche droite
if touches[pygame.K_RIGHT]:
direction.x += 1.0
return {
QUITTER : quitter,
DIRECTION : direction
}
# cette fonction modifie l'état du niveau en fonction
# des entrées du joueur
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
# cette fonction déplace le personnage
# en additionant sa vitesse et direction à sa position
def deplacer_personnage(etat):
etat[PERSONNAGE][POSITION] += etat[PERSONNAGE][VITESSE] * etat[PERSONNAGE][DIRECTION]
# cette fonction fait avancer l'état du jeu d'une frame
# on la découpe en sous-fonctions pour plus de clarté
# Chaque fonction représente une "règle" de fonctionnement
# de la simulation
def simuler_frame(etat):
deplacer_personnage(etat)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION],
20
)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
stop = False
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
#on applique les entrées à l'état
appliquer_entrees_joueur(etat, entrees)
#on avance la simulation
simuler_frame(etat)
afficher_niveau(etat)
pygame.quit()
niveau()
Si vous lancez ce code et maintenez la flèche droite enfoncée, vous devriez voir le personnage se déplacer... très vite.
On verra plus tard quelques réglages. Avant ça, activité :
Activité
Permettez au personnage de se déplacer vers la gauche.
On ajoutes simplement la direction dans la captation des entrées :
Ajoutez le déplacement vertical
Rappel : l'axe y va croissant vers le bas !
il faut modifier la coordonnée y du vecteur direction
Et ne pas oublier que l'axe est INVERSE
Temps réel et précision
Vous avez vu que le personnage se déplace très vite. Et si vous aviez plusieurs ordinateurs, vous verriez aussi que le personnage ne se déplace pas à la même vitesse sur toutes les machines.
C'est parce que pour le moment, on enchaîne les frames sans aucune attente, donc la vitesse de déplacement dépends du nombre de frames par secondes, et donc de la vitesse de la machine sur laquelle s'exécute le jeu.
Commençons par attendre à chaque frame pour essayer d'avoir exactement 60 frames par seconde.
def niveau():
etat = initialiser_niveau()
# l'utilitaire python.time.Clock crée une
# horloge capable de mesurer le temps et
# de faire des attentes
horloge = pygame.time.Clock()
stop = False
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat)
afficher_niveau(etat)
# clock.tick(FPS) mesure le temps écoulé depuis le dernier. FPS = frame par seconde
# appel, et attends de manière à ce que le temps écoulé
# soit de au moins 1/FPS secondes
horloge.tick(60)
pygame.quit()
Remplacez la fonction niveau par celle ci-dessus dans le code, et lancez le jeu. Le personnage devrait se déplacer plus doucement.
Ok, mais... à quelle vitessé précisément ? à quoi correspond 10 dans la vitesse du personnage ?
Des pixels par frame. Ok, mais c'est pas hyper compréhensible ça... On voudrait une unité plus humainement compréhensible. Par exemple, des pixels par seconde. C'est bien plus simple à manipuler : à 100 pixels par seconde, on sait que le personnage traverse l'écran verticalement en 7 secondes environ.
Idéalement, on veut connaître la durée de chaque frame. Et ça tombe bien, horloge.tick retourne la durée réelle mesurée pour la frame courante.
Récupérons sa valeur de retour et affichons là.
On lance le jeu, et on regarde l'affichage en console :
C'est en milisecondes, et vous voyez que c'est pas totalement stable. C'est normal, c'est parce que tick ne peut que garantir que les frames ne durent pas moins d'un certain temps (16ms pour 60 FPS). tick ne peut rien faire si la frame dure plus de 16ms, par exemple à cause d'un lag. Et donc pour que le jeu soit le plus fluide possible, on doit tenir compte de la durée réelle de chaque frame dans la simulation.
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE = "VITESSE"
def creer_personnage():
# On modifie la vitesse pour la valeur que l'on veut en
# pixels par seconde
# j'ai pris 300 parce que ça semble raisonnable;
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage()
}
def entrees_joueur():
quitter = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
# cette fonction renvoie l'état actuel de toutes les
# touches du clavier. Une touche vaut True si elle est
# enfoncée, False sinon
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
# pygame.K_RIGHT est la touche fleche droite
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if touches[pygame.K_UP]:
direction.y += -1.0 #-1.0 parce que axe inversé
if touches[pygame.K_DOWN]:
direction.y -= -1.0
return {
QUITTER : quitter,
DIRECTION : direction
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
# on s'attends maintenant à ce que dt soit en *secondes*
# (voir la fonction niveau pour l'explication)
def deplacer_personnage(etat, dt):
#remarquez le *dt en bout de ligne
etat[PERSONNAGE][POSITION] += etat[PERSONNAGE][VITESSE] * etat[PERSONNAGE][DIRECTION] * dt
# on s'attends maintenant à ce que dt soit en *secondes*
# (voir la fonction niveau pour l'explication)
def simuler_frame(etat, dt):
deplacer_personnage(etat, dt)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION],
20
)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
# l'utilitaire python.time.Clock crée une
# horloge capable de mesurer le temps et
# de faire des attentes
horloge = pygame.time.Clock()
stop = False
#on considère la première frame comme de taille fixe
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
# on passe la durée de la frame à la simulation
simuler_frame(etat, dt)
afficher_niveau(etat)
# ici, /1000 pour passer en secondes
# pour plus de lisibilité dans le reste du code
dt = horloge.tick(60) / 1000
# ici on a retiré le print
pygame.quit()
niveau()
Lancez le jeu et mesurez le temps que prends le personnage à traverser l'écran verticalement. Il devrait être de 2 secondes et demie environ. N'hésitez pas à changer la valeur de vitesse comme vous préférez.
Géométrie et précision
Activité
Lancez le jeu et déplacez le personnage, horizontalement, verticalement, et en diagonale. Remarquez-vous quelque chose ? Essayez d'expliquer pourquoi.
Et oui, le personnage se déplace plus vite en diagonale qu'horizontalement ou verticalement !
La raison, c'est qu'on fait :
Or, si on va disons à droite, direction vaut (1,0), donc de longueur 1. Si on va en diagonale bas-droite, direction vaut (1,1), de longueur racine de 2 (environ 1.4), et donc la vitesse en résultant va plus vite.
Voici comment résoudre ce problème :
def entrees_joueur():
# ...
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if touches[pygame.K_UP]:
direction.y += -1.0
if touches[pygame.K_DOWN]:
direction.y -= -1.0
# normalize retourne une copie normalisée du vecteur,
# c'est à dire avec sa longueur à 1
# et avec la même direction
# on ne peut pas normaliser un vecteur de longueur 0
# C'est pour ca qu'on vérifie avec un if avant.
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction
}
Remplacez la fonction par celle-ci et lancez le jeu. Vous pouvez observer que le personnage se déplace maintenant à vitesse égale dans toutes les directions.
Physique de base : gravité et saut
On va petit à petit transformer le jeu en jeu de plateforme. Donc, le personnage pourra se déplacer horizontalement, et sauter/tomber sur l'axe vertical.
Pour ça, il faut ajouter de la physique de base. La physique conserve la vitesse, ou une partie de la vitesse, entre les frames : c'est l'inertie.
Modifions le code du jeu pour permettre au personnage de sauter et de tomber.
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE = "VITESSE"
INERTIE = "INERTIE"
SAUT = "SAUT"
# on ajoute au personnage une *inertie*. C'est un vecteur
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300,
INERTIE : Vector2(0, 0)
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage()
}
def entrees_joueur():
quitter = False
saut = False
# le saut est géré par un évènement et non par get_pressed.
# C'est plus précis pour les touches que le
# joueur va appuyer pendant peu de temps
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
# ici, on detecte le saut
if evt.key == pygame.K_UP:
saut = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
# On retire la direction verticale
#qui était ici avant
if direction.length() > 0:
direction = direction.normalize()
#on oublie pas de retourner la commande de saut
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut
}
# on traite la commande du saut.
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
# On modifie l'inertie du personnage pour avoir une vitesse
# verticale forte
# La valeur est très élevée parce que la gravitée va continuer à s'appliquer
# pendant le saut.
# Un changement d'inertie brutal s'appelle une *impulsion* dans le monde
# de la simulation physique
# On oublie pas que l'axe y est inversé, d'où le -
etat[PERSONNAGE][INERTIE] = Vector2(0, -500)
# ici, on applique la gravité au personnage
def gravite_personnage(etat, dt):
# on augmente simplement la vitesse vers le bas en fonction du temps passé
# J'ai pas calculé proprement les valeurs ici, j'ai simplement lancé le jeu
# et testé avec plusieurs valeurs. Pareil pour l'impulsion du saut
etat[PERSONNAGE][INERTIE] += Vector2(0, 1000) * dt
# On modifie l'application de la vitesse pour prendre en compte l'inertie
def deplacer_personnage(etat, dt):
# L'inertie est une vitesse comme une autre
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][VITESSE] * etat[PERSONNAGE][DIRECTION] + etat[PERSONNAGE][INERTIE]) * dt
# on oublie pas d'appliquer la gravité
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
deplacer_personnage(etat, dt)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION],
20
)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
horloge = pygame.time.Clock()
stop = False
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat, dt)
afficher_niveau(etat)
dt = horloge.tick(60) / 1000
pygame.quit()
niveau()
Lancez le jeu. Vous devriez voir le personnage tomber. Vous devriez aussi pouvoir sauter avec flèche haut.
Activité
Modifiez la physique du jeu pour qu'on ai l'impression que la gravité est moins forte.
Collision avec le sol
Si vous lancez le jeu et ne faites rien ... Le personnage disparaît sous le sol !
# On détecte quand le personnage sort de l'écran
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > 720:
#et on corrige
etat[PERSONNAGE][POSITION].y = 720
#On annule l'inertie verticale du personnage.
etat[PERSONNAGE][INERTIE].y = 0
# on gère les collisions APRES avoir déplacé le personnage
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_sol(etat, dt)
Activité
Vous avez sans doute remarqué que le cercle du personnage s'enfonce à moitié dans le sol.
Expliquez le problème puis résolvez-le. Il y a au moins deux solutions. L'une est bien meilleure que l'autre.
La position du personnage correspond au centre du cercle, et donc comme la position du personnage bloque au niveau du sol, le cercle, lui, s'enfonce plus.
La première solution, c'est de "décaler" le sol du rayon du cercle personnage (20) vers le haut
if etat[PERSONNAGE][POSITION].y > (720 - 20):
#et on corrige
etat[PERSONNAGE][POSITION].y = 720 - 20
Cette solution marche, mais elle risque de poser problème par la suite : on va devoir décaler toutes les intéractions physiques du rayon du personnage ! Ca peut vite devenir très compliqué...
La seconde solution, c'est de simplement dessiner le cercle "décalé" vers le haut de son rayon. Comme ça, la position du personnage correspondra à la position de ses "pieds"
Plateforme : dessin
Dans un jeu de plateformes, il faut... des plateformes.
Une plateforme est simplement une ligne horizontale. Le joueur peut traverser une plateforme par dessous, mais pas par dessus.
On va procéder par étapes :
- D'abord, on crée UNE plateforme et on la dessine sans qu'elle interagisse avec le joueur
- Ensuite, on gère la collision entre le joueur et la plateforme
- Finalement, on généralise le code à un nombre variable de plateformes.
Bon, commençons par créer une seule plateforme:
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE = "VITESSE"
INERTIE = "INERTIE"
SAUT = "SAUT"
DEBUT = "DEBUT"
FIN = "FIN"
PLATEFORMES = "PLATEFORMES"
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300,
INERTIE : Vector2(0, 0)
}
# Fonction pour créer une plateforme
# Les paramètres permettent de ne rien oublier, par rapport à un dictionaire
# debut et fin sont des coordonnées x
# hauteur est la coordonnée y
def creer_plateforme(debut, fin, hauteur):
return {
DEBUT : Vector2(debut, hauteur),
FIN : Vector2(fin, hauteur)
}
# On ajoute la plateforme au niveau.
# Pour le moment sous forme d'une valeur simple
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage(),
PLATEFORMES : creer_plateforme(100, 300, 500)
}
def entrees_joueur():
quitter = False
saut = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
etat[PERSONNAGE][INERTIE] = Vector2(0, -500)
def gravite_personnage(etat, dt):
etat[PERSONNAGE][INERTIE] += Vector2(0, 1000) * dt
def deplacer_personnage(etat, dt):
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][VITESSE] * etat[PERSONNAGE][DIRECTION] + etat[PERSONNAGE][INERTIE]) * dt
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
etat[PERSONNAGE][POSITION].y = 720
etat[PERSONNAGE][INERTIE].y = 0
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_sol(etat, dt)
# On déplace le code d'affichage du personnage dans un autre
# fonction pour plus de clarté
def afficher_personnage(etat):
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION] + Vector2(0, -20),
20
)
# Et on définit une fonction pour afficher la plateforme
def afficher_plateformes(etat):
#pygame.draw.rect permet de dessiner un rectangle à l'écran :
# surface
# couleur
# Rectangle : Rect(gauche, haut, largeur, hauteur)
pygame.draw.rect(
etat[ECRAN],
(128, 128, 128), #C'est du gris
Rect(
etat[PLATEFORMES][DEBUT].x,
etat[PLATEFORMES][DEBUT].y,
etat[PLATEFORMES][FIN].x - etat[PLATEFORMES][DEBUT].x,
10)
)
# on dessine la ligne comme un rectangle, dont le bord haut
# est aligné avec la ligne et de quelques pixels d'epaisseur (ici, 10)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
# On affiche la plateforme AVANT d'afficher le personnage, de cette manière en cas de superposition
# c'est le personnage qui sera devant
afficher_plateformes(etat)
afficher_personnage(etat)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
horloge = pygame.time.Clock()
stop = False
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat, dt)
afficher_niveau(etat)
dt = horloge.tick(60) / 1000
pygame.quit()
niveau()
Si vous lancez le jeu, vous verrez apparaître une plateforme :

Plateforme : collision
Maintenant, il faut détecter une collision entre le joueur et la plateforme.
Là, pas le choix, c'est DES MATHS.
C'est un peu plus compliqué que les collisions avec le sol, parce que la plateforme n'est pas infinie, dans les deux directions :
- Le joueur ne peut pas aller "dans le sol" donc à partir du moment où il est sous le sol, on sait qu'il y a collision
- Le sol est infini horizontalement, donc la vérification est très simple.
Pour la plateforme, on va devoir vérifier si le segment qu'a parcouru le personnage dans son déplacement dans la frame a une intersection avec le segment de la plateforme.
Il y a plusieurs manières de connaître l'existence d'une intersection entre deux segments dont un des deux est toujours horizontal. Je vous présente ici UNE méthode. Ce n'est pas forcément la meilleure, mais elle a le mérite d'être plus visuelle et donc plus simple à expliquer graphiquement
On va réaliser des projections des deux segments sur différentes droites.
Vocabulaire : Projection
Ici, \(v'\) est la projection orthogonale, ou simplement projection de \(v\) sur \(u\).
Précisément 3 :
- L'axe y
- L'axe x
- une droite perpendiculaire au segment du mouvement
Si les projections des deux segments se superposent sur TOUTES ces droites, alors il y a intersection, sinon, il n'y a pas intersection.
En Python, ça donne ça :
# Effectue la projection du segment avec début et fin sur axe, un vecteur normalisé
# Retourne un couple de valeur d,f avec d <= f, qui corresponds à la position de début
# et fin sur l'axe
# On retrouve les projections de debut et fin en faisant
# axe * d et axe * f
def projection_segment(axe, debut, fin):
# v1.project(v2) retourne la projection orthogonale de v1 sur v2.
debut_proj = debut.project(axe)
# On convertis la projection en une coordonnée sur l'axe
d = debut_proj.x / axe.x if axe.x != 0 else debut_proj.y / axe.y
fin_proj = fin.project(axe)
f = fin_proj.x / axe.x if axe.x != 0 else fin_proj.y / axe.y
# on retourne les deux dans le bon ordre
return (
min(d, f),
max(d, f)
)
# pour deux segments (debut1, fin1), (debut2, fin2) des couples de vecteurs et un axe, retourne True si
# les projections de ces segments se superposent sur l'axe
def proj_superposees(axe, debut1, fin1, debut2, fin2):
p1 = projection_segment(axe, debut1, fin1)
p2 = projection_segment(axe, debut2, fin2)
# pas de superposition si la fin de l'un est plus grande que le début de l'autre
return not (p1[1] < p2[0] or p2[1] < p1[0])
# a partir d'un vecteur non nul vec, retourne un vecteur non nul orthognal (perpendiculaire) à vec, normalisé
def vec_ortho(vec):
return vec.rotate(90).normalize()
# A partir d'une forme (debut_p, fin_p) et d'un déplacement entre debut_m et fin_m des vecteurs,
# Retourne True si le déplacement entre en collision avec la plateforme
def collision_plateforme(debut_p, fin_p, debut_m, fin_m):
# pas de collision si déplacement nul
if (debut_m - fin_m).length() == 0:
return False
#sinon, on teste les 3 axes : x, y, orthogonal au déplacement
deplacement = fin_m - debut_m
axes = [Vector2(0, 1), Vector2(1, 0), vec_ortho(deplacement)]
for axe in axes:
# si un axe n'a pas de superposition, pas d'intersection
if not proj_superposees(axe, debut_p, fin_p, debut_m, fin_m):
return False
# si tous superposent, intersection
return True
On peut maintenant ajouter la détection de collision pour la plateforme:
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE = "VITESSE"
INERTIE = "INERTIE"
SAUT = "SAUT"
DEBUT = "DEBUT"
FIN = "FIN"
PLATEFORMES = "PLATEFORMES"
POSITION_DEBUT_FRAME = "POSITION_DEBUT_FRAME"
# On ajoute une manière de se rappeler de la position du perso en début de frame
# On en aura besoin pour retracer le déplacement du personnage dans la frame
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300,
INERTIE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0)
}
def creer_plateforme(debut, fin, hauteur):
return {
DEBUT : Vector2(debut, hauteur),
FIN : Vector2(fin, hauteur)
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage(),
PLATEFORMES : creer_plateforme(100, 300, 500)
}
def entrees_joueur():
quitter = False
saut = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
etat[PERSONNAGE][INERTIE] = Vector2(0, -500)
def gravite_personnage(etat, dt):
etat[PERSONNAGE][INERTIE] += Vector2(0, 1000) * dt
# On se rappelle de la position du personnage en début de frame
def deplacer_personnage(etat, dt):
# On fait une copie du vecteur, sinon il sera modifié quand on modifiera la position
etat[PERSONNAGE][POSITION_DEBUT_FRAME] = Vector2(etat[PERSONNAGE][POSITION])
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][VITESSE] * etat[PERSONNAGE][DIRECTION] + etat[PERSONNAGE][INERTIE]) * dt
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
etat[PERSONNAGE][POSITION].y = 720
etat[PERSONNAGE][INERTIE].y = 0
def projection_segment(axe, debut, fin):
debut_proj = debut.project(axe)
d = debut_proj.x / axe.x if axe.x != 0 else debut_proj.y / axe.y
fin_proj = fin.project(axe)
f = fin_proj.x / axe.x if axe.x != 0 else fin_proj.y / axe.y
return (
min(d, f),
max(d, f)
)
def proj_superposees(axe, debut1, fin1, debut2, fin2):
p1 = projection_segment(axe, debut1, fin1)
p2 = projection_segment(axe, debut2, fin2)
return not (p1[1] < p2[0] or p2[1] < p1[0])
def vec_ortho(vec):
return vec.rotate(90).normalize()
def collision_plateforme(debut_p, fin_p, debut_m, fin_m):
if (debut_m - fin_m).length() == 0:
return False
deplacement = fin_m - debut_m
axes = [Vector2(0, 1), Vector2(1, 0), vec_ortho(deplacement)]
for axe in axes:
if not proj_superposees(axe, debut_p, fin_p, debut_m, fin_m):
return False
return True
# On vérifie la collision avec la plateforme et on réagit si besoin
# Il ne peut avoir une collision que si le joueur DESCENDS
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][INERTIE].y > 0:
if collision_plateforme(
etat[PLATEFORMES][DEBUT],
etat[PLATEFORMES][FIN],
etat[PERSONNAGE][POSITION_DEBUT_FRAME],
etat[PERSONNAGE][POSITION]
) :
etat[PERSONNAGE][POSITION].y = etat[PLATEFORMES][DEBUT].y
#On remets l'inertie verticale du personnage à 0
etat[PERSONNAGE][INERTIE].y = 0
# on vérifie la collision de la plateforme AVANT le sol
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
def afficher_personnage(etat):
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION] + Vector2(0, -20),
20
)
def afficher_plateformes(etat):
pygame.draw.rect(
etat[ECRAN],
(128, 128, 128),
Rect(
etat[PLATEFORMES][DEBUT].x,
etat[PLATEFORMES][DEBUT].y,
etat[PLATEFORMES][FIN].x - etat[PLATEFORMES][DEBUT].x,
10)
)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
afficher_plateformes(etat)
afficher_personnage(etat)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
horloge = pygame.time.Clock()
stop = False
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat, dt)
afficher_niveau(etat)
dt = horloge.tick(60) / 1000
pygame.quit()
niveau()
Plateforme : plusieurs plateformes
Activité
Modifiez le code du jeu pour ajouter plusieurs plateformes, qui vont toutes donner au joueur
Il faut transformer la plateforme en une liste de plateformes, et adapter le code de collision_plateformes et de
dessiner_plateformes en conséquences.
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE = "VITESSE"
INERTIE = "INERTIE"
SAUT = "SAUT"
DEBUT = "DEBUT"
FIN = "FIN"
PLATEFORMES = "PLATEFORMES"
POSITION_DEBUT_FRAME = "POSITION_DEBUT_FRAME"
# On ajoute une manière de se rappeler de la position du perso en début de frame
# On en aura besoin pour retracer le déplacement du personnage dans la frame
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300,
INERTIE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0)
}
def creer_plateforme(debut, fin, hauteur):
return {
DEBUT : Vector2(debut, hauteur),
FIN : Vector2(fin, hauteur)
}
# On transforme la plateforme en une liste de plateformes
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage(),
PLATEFORMES : [
creer_plateforme(100, 300, 500),
creer_plateforme(100, 300, 200),
creer_plateforme(800, 900, 500),
creer_plateforme(1000, 1100, 400),
creer_plateforme(500, 700, 600),
]
}
def entrees_joueur():
quitter = False
saut = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
etat[PERSONNAGE][INERTIE] = Vector2(0, -500)
def gravite_personnage(etat, dt):
etat[PERSONNAGE][INERTIE] += Vector2(0, 1000) * dt
# On se rappelle de la position du personnage en début de frame
def deplacer_personnage(etat, dt):
# On fait une copie du vecteur, sinon il sera modifié quand on modifiera la position
etat[PERSONNAGE][POSITION_DEBUT_FRAME] = Vector2(etat[PERSONNAGE][POSITION])
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][VITESSE] * etat[PERSONNAGE][DIRECTION] + etat[PERSONNAGE][INERTIE]) * dt
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
etat[PERSONNAGE][POSITION].y = 720
etat[PERSONNAGE][INERTIE].y = 0
def projection_segment(axe, debut, fin):
debut_proj = debut.project(axe)
d = debut_proj.x / axe.x if axe.x != 0 else debut_proj.y / axe.y
fin_proj = fin.project(axe)
f = fin_proj.x / axe.x if axe.x != 0 else fin_proj.y / axe.y
return (
min(d, f),
max(d, f)
)
def proj_superposees(axe, debut1, fin1, debut2, fin2):
p1 = projection_segment(axe, debut1, fin1)
p2 = projection_segment(axe, debut2, fin2)
return not (p1[1] < p2[0] or p2[1] < p1[0])
def vec_ortho(vec):
return vec.rotate(90).normalize()
def collision_plateforme(debut_p, fin_p, debut_m, fin_m):
if (debut_m - fin_m).length() == 0:
return False
deplacement = fin_m - debut_m
axes = [Vector2(0, 1), Vector2(1, 0), vec_ortho(deplacement)]
for axe in axes:
if not proj_superposees(axe, debut_p, fin_p, debut_m, fin_m):
return False
return True
# On teste maintenant la collision avec chaque plateforme
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][INERTIE].y > 0:
for plateforme in etat[PLATEFORMES]:
if collision_plateforme(
plateforme[DEBUT],
plateforme[FIN],
etat[PERSONNAGE][POSITION_DEBUT_FRAME],
etat[PERSONNAGE][POSITION]
) :
etat[PERSONNAGE][POSITION].y = plateforme[DEBUT].y
etat[PERSONNAGE][INERTIE].y = 0
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
def afficher_personnage(etat):
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION] + Vector2(0, -20),
20
)
# On affiche toutes les plateformes
def afficher_plateformes(etat):
for plateforme in etat[PLATEFORMES]:
pygame.draw.rect(
etat[ECRAN],
(128, 128, 128),
Rect(
plateforme[DEBUT].x,
plateforme[DEBUT].y,
plateforme[FIN].x - plateforme[DEBUT].x,
10)
)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
afficher_plateformes(etat)
afficher_personnage(etat)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
horloge = pygame.time.Clock()
stop = False
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat, dt)
afficher_niveau(etat)
dt = horloge.tick(60) / 1000
pygame.quit()
niveau()
Game design
Un illustre concepteur de jeu vidéo, Nolan Bushnell, a un jour dit un truc du genre :
Un bon jeu vidéo est facile à apprendre et difficile à maîtriser.
L'idée, c'est qu'un bon jeu se base sur des principes relativement simple et/ou guide les joueurs et joueuses dans leurs apprentissage, et propose ensuite des défi pour tous les niveaux de maîtrise de ces principes.
Relativité
Idéalement, un jeu doit être accessible à tous et toutes, mais...
Ce principe n'est pas absolu, mais relatif en fonction du public cible.
En particulier, ce qui est "facile à apprendre" dépends beaucoup des joueurs et joueuses qui sont la cible principale du jeu.
Par exemple, beaucoup de mécaniques que l'on trouve dans les MMORPGs sont compliquées : guildes, raides, instances, crafting, compétences, ... Mais comme elles sont dans presque TOUS les MMORPGs, les joueurs et joueuses de MMO y sont habitués, et donc il leur est facile de les apprendre.
Un bon exemple d'un jeu respectant un tel principe est le jeu de rythme osu!. Il consiste simplement à cliquer en rythme sur des cercles qui apparaissent à l'écran alors qu'une musique joue en arrière plan. C'est simple et intuitif. Mais la difficulté augmente plus ou moins infiniement jusqu'à ce genre de non-sens que seulement quelques personnes dans le monde sont capable de réussir.
Bon, notre but c'est de faire un jeu. Il nous faut donc:
- Un objectif
- Des mécaniques pour y arriver
- Des moyens de régler la difficulté et de proposer du défi à tous les niveaux de maîtrise des mécaniques
Le choix de ces trois aspects est influencé par ce que l'on veut faire, mais aussi par ce que l'on peut faire.
Comme ce cours veut rester relativement simple, ces aspects vont rester relativement simples.
L'objectif pour le joueur sera d'atteindre un point du niveau situé en hauteur.
Les mécaniques seront :
- Le saut
- Le dash, un mouvement horizontal rapide
- Le boost, Une sorte de second saut, qui annule les déplacements horizontaux mais pas verticaux du personnage.
La difficulté sera réglée :
- La disposition des plateformes (taille, position, nombre)
- Une vitesse minimale d'ascension requise pour le joueur (par exemple, de la lave qui monte)
- Eventuellement, des obstacles que le joueur doit éviter
Bien sûr, à la fin du chapitre, vous pourrez modifier le jeu comme vous le souhaitez...
Dash
Le dash est un déplacement horizontal rapide.
Il y a des tas de manières d'implémenter un dash :
- Téléporter le personnage dans la direction où il se déplace
- Augmenter la vitesse de "course/marche" du personnage pendant un court laps de temps
- Donner une impulsion horizontale au personnage, comme un "saut" horizontal.
- ...
C'est l'impulsion qui va nous intéresser ici, parce que jouer avec la physique va permettre par la suite de faire des combos avec le boost.
Activité : impulsion naive
Ajoutez un dash que le joueur peut utiliser en appuyant sur la touche MAJ GAUCHE (de code pygame.K_LSHIFT).
Ne vous préoccupez pas des détails, ajoutez simplement une impulsion horizontale.
Je ne montre ici que les fonctions qui a ont été modifiés
# On ajoute une clé dash
DASH = "DASH"
def entrees_joueur():
quitter = False
saut = False
dash = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
# On détecte un appui sur la touche de dash
if evt.key == pygame.K_LSHIFT:
dash = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut,
DASH : dash #on rends compte de l'appui
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
etat[PERSONNAGE][INERTIE] = Vector2(0, -500)
# Si le personnage dash, on fait une impulsion horitontale dans sa direction de déplacement
if entrees[DASH]:
etat[PERSONNAGE][INERTIE] = etat[PERSONNAGE][DIRECTION] * 500
Ca marche... si on veut Uu
En particulier, vous avez sans doute remarqué 3 choses :
- Le saut interrompt le dash et vice-versa. C'est pas très combo tout ça !
- Le dash est affecté par la gravité. Ca rends le dash un peu bizarre, parce que le personnage "tombe".
- Si on dash une fois et qu'on laisse aller, le personnage se déplace continuellement dans la direction. Ca rends le dash très bizarre, parce qu'on voudrait que le personnage s'arrête au bout d'un temps.
Il va falloir résoudre ces problèmes, un par un.
Combos
Activité
Modifiez la physique du jeu (saut, gravité, dash, ...) pour que les mouvements du dash et du saut s'additionnent au lieu de s'annuler.
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
# le saut annule l'inertie verticale, mais pas l'inertie horizontale :
etat[PERSONNAGE][INERTIE] = Vector2(etat[PERSONNAGE][INERTIE].x, -500)
if entrees[DASH]:
# Le dash annule l'inertie horizontale, mais pas l'inertie verticale :
etat[PERSONNAGE][INERTIE] = Vector2( etat[PERSONNAGE][DIRECTION].x * 500, etat[PERSONNAGE][INERTIE].y)
Frottements
Pour que l'impulsion du dash disparaisse au bout d'un temps, il y a deux manières :
- Annuler l'impulsion ou appliquer une impulsion inverse après un certain temps.
- Réduire continuellement l'intertie du personnage, sous forme de "frottements".
Activité
Annuler l'impulsion (ou appliquer une impulsion inverse) après un certain temps peut poser problème si on veut ajouter d'autres intéraction physiques par la suite. Essayez de trouver un scénario exemple.
Imaginons que l'on ajoute une autre compétence qui modifie l'inertie horizontale du personnage. On pourrait avoir le scénario suivant :
- Le personnage a une vitesse horizontale de 0.
- Il dash, sa vitesse devient 500.
- Il active la compétence de frein, sa vitesse devient 250.
- On applique l'impulsion inverse au dash, sa vitesse est maintenant -250. C'est incohérent.
On peut avoir aussi :
- Le personnage a une vitesse horizontale de 0.
- Il dash, sa vitesse devient 500
- Il applique un boost, sa vitesse devient 750.
- On annule sa vitesse horizontale, qui devient 0 : le boost a été perdu dans le processus.
On va donc plutôt appliquer un frottement, comme dans la vraie vie.
Le frottement est une diminution de la vitesse proportionnelle à la vitesse : plus on va vite, plus les frottements sont forts et donc plus la vitesse diminue vite.
Pour ça, on donne au personnage un coefficient de frottement. C'est une valeur comprise entre 0 et 1, qui exprime la force générale des frottements. 0 corresponds à aucun frottement, et 1 à une "accroche" totale, qui annule instantanément la vitesse.
La formule de diminution de la vitesse que l'on utilise est la suivante :
Avec :
V_fla vitesse après application des frottementsV_ila vitesse initialeC_fle coefficient de frottementtl'intervalle de temps sur lequel les frottements sont appliqués.
En python, ça donne :
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300,
INERTIE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0),
#Valeur un peu arbitraire, on la réglera plus tard
COEF_FROTTEMENT : 0.3
}
def frottements_personnage(etat, dt):
# On ne gère que l'axe horizontal pour le moment
etat[PERSONNAGE][INERTIE].x = etat[PERSONNAGE][INERTIE].x * (1 - etat[PERSONNAGE][COEF_FROTTEMENT]) ** dt
# si le personnage a beaucoup ralenti horizontalement, on annule sa vitesse
# abs est la valeur absolue
if abs(etat[PERSONNAGE][INERTIE].x) < 1:
etat[PERSONNAGE][INERTIE].x = 0
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
# on oublie pas d'appeler la fonction !
frottements_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
Modifiez la valeur de l'impulsion du dash et du coefficient de frottement jusqu'à arriver à un comportement qui vous convient.
Personnellement, j'ai monté le dash à 1000 et le frottement à 0.9.
Vous avez peut-être remarqué quelque-chose un peu étrange avec le dash :
- Dashez vers la gauche
- Déplacez-vous un coup vers la droite très rapidement
- puis relachez le déplacement assez rapidement aussi
Le personnage se remet à aller vers la gauche.
C'est parce que le déplacement du personnage n'affecte pas son inertie, et vice versa.
Pour résoudre ça, on doit gérer les déplacements du personnage directement avec la physique.
Moteur
Au lieu d'avoir une vitesse "séparée", notre personnage va se déplacer à l'aide d'impulsions.
def deplacer_personnage(etat, dt):
#On modifie l'inertie du personnage en appliquant une impulsion
etat[PERSONNAGE][INERTIE] += etat[PERSONNAGE][DIRECTION] * etat[PERSONNAGE][VITESSE]
etat[PERSONNAGE][POSITION_DEBUT_FRAME] = Vector2(etat[PERSONNAGE][POSITION])
#Il n'y a plus que l'inertie qui compte, on retire la vitesse
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][INERTIE]) * dt
Si on essaie le code ici, on remarque un problème : le personnage accélère encore et encore quand on essaie de le déplacer.
Il faut donc que les déplacements du personnage se comportent comme un moteur :
- On fixe une force maximale que le moteur peut appliquer
- On donne une vitesse cible au moteur
- Le moteur va appliquer sa force sur le personnage pour modifier son inertie et essayer d'atteindre la vitesse cible, sans dépasser la valeur maximale.
#nouvelle clé
FORCE_MOTEUR = "FORCE_MOTEUR"
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE : 300,
INERTIE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0),
COEF_FROTTEMENT : 0.9,
#La force du moteur
FORCE_MOTEUR : 100,
}
def moteur_personnage(etat, dt):
#On calcule l'impulsion max a partir de la force. Une impulsion est une force * du temps
#donc dans l'intervalle dt, on peut avoir une impulsion de au max :
impulsion_max = etat[PERSONNAGE][FORCE_MOTEUR] * dt
#on calcule la vitesse cible :
vitesse_cible = etat[PERSONNAGE][DIRECTION].x * etat[PERSONNAGE][VITESSE]
#On calcule maintenant l'impulsion idéale pour atteindre cette vitesse :
impulsion_ideale = vitesse_cible - etat[PERSONNAGE][INERTIE].x
#si l'impulsion idéale est inférieure à l'impulsion max, on l'applique directement :
if abs(impulsion_ideale) <= impulsion_max:
etat[PERSONNAGE][INERTIE].x += impulsion_ideale
else :
# sinon, on applique l'impulsion max :
# que l'on met dans le bon sens évidemment
if impulsion_ideale < 0 :
impulsion_max = -impulsion_max
etat[PERSONNAGE][INERTIE].x += impulsion_max
def deplacer_personnage(etat, dt):
#L'impulsion est maintenant dans le moteur
etat[PERSONNAGE][POSITION_DEBUT_FRAME] = Vector2(etat[PERSONNAGE][POSITION])
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][INERTIE]) * dt
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
frottements_personnage(etat, dt)
#on oublie pas d'appeler le moteur
moteur_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
On essaie, et c'est LEEEEEENT. Parce qu'une telle force est beaucoup trop petite dans l'idée.
Il faut donc jouer avec les différentes valeurs : - Force du moteur - Coefficient frottement - Impulsion du dash
Pour arriver à une valeur correcte.
Personnellement, j'ai mis 3000 dans l'impulsion du dash, et 10000 en force du moteur, c'est NERVEUX mdrr
Petit refactoring
Quand on programme, il est crucial de garder un code propre, c'est à dire facile à comprendre.
On peut faire deux actions pour ça :
- Réfléchir avant d'écrire le code pour qu'il soit propre. C'est la conception. Mais il y a toujours des défauts qu'on ne peut pas anticiper.
- Faire le ménage dans le code régulièrement, pour corriger les défauts qu'on a repéré au fur et à mesure, c'est le refactoring.
Le code du jeu comporte a minima deux gros défauts qui peuvent être sources de confusion :
Activité : imaginons
Imaginons que quelqu'un qui n'a pas encore participé au projet lit votre code :
- Quelqu'un a qui vous avez demandé de l'aide
- Ou même le "vous" de dans quelques semaines quand vous reprendrez le code après une pause
Que comprendra cette personne en lisant les termes VITESSE et INERTIE ?
Ces définitions correspondent à l'usage qui en est fait dans le code ?
Allons chercher des définitions usuelles de vitesse et inertie. Wikipédia est une plutôt bonne source pour ça.
vitesse : la vitesse est une grandeur qui mesure, pour un mouvement, le rapport de la distance parcourue au temps écoulé.
inertie : En physique, l'inertie d'un corps est sa tendance à conserver sa vitesse.
Aie. En fait, ce qu'on a appelé inertie dans le code, c'est la vitesse du personnage. Et l'inertie se manifeste dans le fait que cette vitesse est conservée entre les frames.
Et ce qu'on a appelé vitesse dans le code, c'est pas exactement la vitesse du personnage, c'est sa vitesse de course.
On va donc changer ces termes dans le code
On va faire VITESSE -> VITESSE_COURSE, puis INERTIE -> VITESSE.
Activité
A votre avis, pourquoi c'est beaucoup plus facile de faire le changement dans cet ordre que dans l'autre ?
Si on change Inertie par Vitesse, on n'arrivera plus à différencier la vitesse (ex inertie) et la vitesse (qui doit devenir vitesse course)
En général votre éditeur a des outils qui aident au refactoring. Dans VSCode, vous pouvez faire :
- Selectionner un mot
- Clic droit sur la selection
- Cliquer sur "Changer toutes les occurrences"
- Selectionner les critères de restriction qui apparaissent en haut à droite de la fenêtre (mots uniquement et sensibilité à la casse)
- Réécrire le mot que vous voulez changer.
Voici le code complet après refactoring:
CODE
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE_COURSE = "VITESSE_COURSE"
VITESSE = "VITESSE"
SAUT = "SAUT"
DEBUT = "DEBUT"
FIN = "FIN"
PLATEFORMES = "PLATEFORMES"
POSITION_DEBUT_FRAME = "POSITION_DEBUT_FRAME"
DASH = "DASH"
COEF_FROTTEMENT = "COEF_FROTTEMENT"
FORCE_MOTEUR = "FORCE_MOTEUR"
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE_COURSE : 300,
VITESSE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0),
COEF_FROTTEMENT : 0.9,
#La force du moteur
FORCE_MOTEUR : 10000,
}
def creer_plateforme(debut, fin, hauteur):
return {
DEBUT : Vector2(debut, hauteur),
FIN : Vector2(fin, hauteur)
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage(),
PLATEFORMES : [
creer_plateforme(100, 300, 500),
creer_plateforme(100, 300, 200),
creer_plateforme(800, 900, 500),
creer_plateforme(1000, 1100, 400),
creer_plateforme(500, 700, 600),
]
}
def entrees_joueur():
quitter = False
saut = False
dash = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
if evt.key == pygame.K_LSHIFT:
dash = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut,
DASH : dash
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][VITESSE].x, -500)
if entrees[DASH]:
etat[PERSONNAGE][VITESSE] = Vector2( etat[PERSONNAGE][DIRECTION].x * 3000, etat[PERSONNAGE][VITESSE].y)
def gravite_personnage(etat, dt):
etat[PERSONNAGE][VITESSE] += Vector2(0, 1000) * dt
def frottements_personnage(etat, dt):
etat[PERSONNAGE][VITESSE].x = etat[PERSONNAGE][VITESSE].x * (1 - etat[PERSONNAGE][COEF_FROTTEMENT]) ** dt
if abs(etat[PERSONNAGE][VITESSE].x) < 1 :
etat[PERSONNAGE][VITESSE].x = 0
def moteur_personnage(etat, dt):
impulsion_max = etat[PERSONNAGE][FORCE_MOTEUR] * dt
vitesse_cible = etat[PERSONNAGE][DIRECTION].x * etat[PERSONNAGE][VITESSE_COURSE]
impulsion_ideale = vitesse_cible - etat[PERSONNAGE][VITESSE].x
if abs(impulsion_ideale) <= impulsion_max:
etat[PERSONNAGE][VITESSE].x += impulsion_ideale
else :
if impulsion_ideale < 0 :
impulsion_max = -impulsion_max
etat[PERSONNAGE][VITESSE].x += impulsion_max
def deplacer_personnage(etat, dt):
#L'impulsion est maintenant dans le moteur
etat[PERSONNAGE][POSITION_DEBUT_FRAME] = Vector2(etat[PERSONNAGE][POSITION])
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][VITESSE]) * dt
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
etat[PERSONNAGE][POSITION].y = 720
etat[PERSONNAGE][VITESSE].y = 0
def projection_segment(axe, debut, fin):
debut_proj = debut.project(axe)
d = debut_proj.x / axe.x if axe.x != 0 else debut_proj.y / axe.y
fin_proj = fin.project(axe)
f = fin_proj.x / axe.x if axe.x != 0 else fin_proj.y / axe.y
return (
min(d, f),
max(d, f)
)
def proj_superposees(axe, debut1, fin1, debut2, fin2):
p1 = projection_segment(axe, debut1, fin1)
p2 = projection_segment(axe, debut2, fin2)
return not (p1[1] < p2[0] or p2[1] < p1[0])
def vec_ortho(vec):
return vec.rotate(90).normalize()
def collision_plateforme(debut_p, fin_p, debut_m, fin_m):
if (debut_m - fin_m).length() == 0:
return False
deplacement = fin_m - debut_m
axes = [Vector2(0, 1), Vector2(1, 0), vec_ortho(deplacement)]
for axe in axes:
if not proj_superposees(axe, debut_p, fin_p, debut_m, fin_m):
return False
return True
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][VITESSE].y > 0:
for plateforme in etat[PLATEFORMES]:
if collision_plateforme(
plateforme[DEBUT],
plateforme[FIN],
etat[PERSONNAGE][POSITION_DEBUT_FRAME],
etat[PERSONNAGE][POSITION]
) :
etat[PERSONNAGE][POSITION].y = plateforme[DEBUT].y
etat[PERSONNAGE][VITESSE].y = 0
def simuler_frame(etat, dt):
gravite_personnage(etat, dt)
frottements_personnage(etat, dt)
moteur_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
def afficher_personnage(etat):
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION] + Vector2(0, -20),
20
)
def afficher_plateformes(etat):
for plateforme in etat[PLATEFORMES]:
pygame.draw.rect(
etat[ECRAN],
(128, 128, 128),
Rect(
plateforme[DEBUT].x,
plateforme[DEBUT].y,
plateforme[FIN].x - plateforme[DEBUT].x,
10)
)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
afficher_plateformes(etat)
afficher_personnage(etat)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
horloge = pygame.time.Clock()
stop = False
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat, dt)
afficher_niveau(etat)
dt = horloge.tick(60) / 1000
pygame.quit()
niveau()
Boost
Maintenant que le dash est implémenté, reste le boost.
Le boost est simplement un autre saut qui annule la vitesse horizontale du personnage.
Activité
Ajoutez le boost dans le jeu, déclenché avec la touche espace, de code pygame.K_SPACE.
Il faut modifier :
- La fonction de captage des entrées
- la fonction d'application des entrées
BOOST = "BOOST" #on ajoute une clé "boost"
def entrees_joueur():
quitter = False
saut = False
dash = False
# On ajoute une valeur pour le boost
boost = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
if evt.key == pygame.K_LSHIFT:
dash = True
#Déclenché avec espace
if evt.key == pygame.K_SPACE:
boost = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut,
DASH : dash,
BOOST : boost #on retourne simplement
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT]:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][VITESSE].x, -500)
if entrees[DASH]:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][DIRECTION].x * 3000, etat[PERSONNAGE][VITESSE].y)
# Le boost annule la vitesse horizontale, mais pas la vitesse verticale
# Et donne une impulsion vers le haut (rappel : l'axe y est inversé !)
# La valeur ici est un peu arbitraire, j'ai choisi une valeur qui donne, en gros, la hauteur de 2 sauts.
if entrees[BOOST]:
etat[PERSONNAGE][VITESSE] = Vector2(0, etat[PERSONNAGE][VITESSE].y - 700)
Limites d'usage
Le but d'un jeu est de créer du défi. Or, pour le moment, il est trop simple, puisque le personnage peut sauter/dasher/booster à l'infini.
On va donc contraindre un peu l'utilisation du dash, du saut et du boost :
- Le personnage peut sauter deux fois et dasher une fois. Il regagne ses sauts/dashs quand il touche le sol (ou une plateforme).
- Le personnage peut booster une fois toutes les 6 secondes.
Ces valeurs sont pour le moment un peu arbitraires, vous pourrez les adapter par la suite.
Activité
Limitez le nombre de sauts à 2 et de dash à 1 entre deux contacts avec une plateforme/le sol.
Commencez par limiter seulement le dash et/ou le saut.
Ajoutez des compteurs dans les données de Personnage. Décrémentez-les quand le personnage fait une action, et redonnez-leur leurs valeurs de base quand il entre en contact avec le sol ou avec une plateforme.
Il vous faudra modifier :
- Ajouter des clés en haut du fichier
creer_personnagecollision_solcollision_plateformesappliquer_entrees_joueur
# Deux nouvelles clés
SAUTS_RESTANTS = "SAUTS_RESTANTS"
DASHS_RESTANTS = "DASH_RESTANTS"
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE_COURSE : 300,
VITESSE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0),
COEF_FROTTEMENT : 0.9,
FORCE_MOTEUR : 10000,
# On donne les valeurs de base
DASHS_RESTANTS : 1,
SAUTS_RESTANTS : 2
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
# On saute seulement si on a des sauts restants
if entrees[SAUT] and etat[PERSONNAGE][SAUTS_RESTANTS] > 0:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][VITESSE].x, -500)
etat[PERSONNAGE][SAUTS_RESTANTS] -= 1 #on diminue le nombre de sauts restants
# On dash seulement si on a des dash restants
if entrees[DASH] and etat[PERSONNAGE][DASHS_RESTANTS] > 0:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][DIRECTION].x * 3000, etat[PERSONNAGE][VITESSE].y)
etat[PERSONNAGE][DASHS_RESTANTS] -= 1 # on diminue le nombre de dashs restants
if entrees[BOOST]:
etat[PERSONNAGE][VITESSE] = Vector2(0, etat[PERSONNAGE][VITESSE].y - 700)
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
etat[PERSONNAGE][POSITION].y = 720
etat[PERSONNAGE][VITESSE].y = 0
# En cas de collision avec le sol, on réinitialise le dash et le saut
etat[PERSONNAGE][SAUTS_RESTANTS] = 2
etat[PERSONNAGE][DASHS_RESTANTS] = 1
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][VITESSE].y > 0:
for plateforme in etat[PLATEFORMES]:
if collision_plateforme(
plateforme[DEBUT],
plateforme[FIN],
etat[PERSONNAGE][POSITION_DEBUT_FRAME],
etat[PERSONNAGE][POSITION]
) :
etat[PERSONNAGE][POSITION].y = plateforme[DEBUT].y
etat[PERSONNAGE][VITESSE].y = 0
# En cas de collision avec une plateforme, on réinitialise le dash et le saut
etat[PERSONNAGE][SAUTS_RESTANTS] = 2
etat[PERSONNAGE][DASHS_RESTANTS] = 1
Maintenant ajoutons un délais minimum entre deux utilisations du boost. Dans le monde du jeu vidéo, on appelle ça un temps de relance ou cooldown.
Activité
Ajoutez un cooldown de 6 secondes pour le boost.
Le principe est la suivant :
Ajoutez une valeur COOLDOWN_BOOST aux données du personnage
De base, sa valeur est 0. Quand la valeur est à 0, le personnage peut utiliser le boost.
Si le personnage utilise le boost, la valeur du cooldown passe à 6.
Chaque frame :
- Si le cooldown est supérieur à 0, il est décrémenté du dt de la frame. Si il passe sous 0, il est fixé à 0.
- si le cooldown est 0, on ne fait rien.
Je vous conseille de créer une nouvelle fonction
Qui sera appelée parsimuler_frame.
#nouvelle clé
COOLDOWN_BOOST = "COOLDOWN_BOOST"
# On l'ajoute au personnage
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE_COURSE : 300,
VITESSE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0),
COEF_FROTTEMENT : 0.9,
FORCE_MOTEUR : 10000,
DASHS_RESTANTS : 1,
SAUTS_RESTANTS : 2,
# Ici, valeur de base 0
COOLDOWN_BOOST : 0
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT] and etat[PERSONNAGE][SAUTS_RESTANTS] > 0:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][VITESSE].x, -500)
etat[PERSONNAGE][SAUTS_RESTANTS] -= 1
if entrees[DASH] and etat[PERSONNAGE][DASHS_RESTANTS] > 0:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][DIRECTION].x * 3000, etat[PERSONNAGE][VITESSE].y)
etat[PERSONNAGE][DASHS_RESTANTS] -= 1
# On peut booster que si le cooldown est inférieur
# ou égal a 0 (le inférieur n'est nécessaire a priori
# mais tant qu'a faire autant le rajouter)
if entrees[BOOST] and etat[PERSONNAGE][COOLDOWN_BOOST] <= 0:
etat[PERSONNAGE][VITESSE] = Vector2(0, etat[PERSONNAGE][VITESSE].y - 700)
# Quand on booste, on met le cooldown a 6
etat[PERSONNAGE][COOLDOWN_BOOST] = 6.0
#fonction pour gérer le cooldown
def cooldowns_personnage(etat, dt):
# si le cooldown est sup a 0, on diminue
if etat[PERSONNAGE][COOLDOWN_BOOST] > 0:
etat[PERSONNAGE][COOLDOWN_BOOST] -= dt
# si il est inférieur on le met a 0
# c'est pas un elif mais un autre if,
# parce qu'il doit vérifier après avoir
# décrémenté
if etat[PERSONNAGE][COOLDOWN_BOOST] < 0:
etat[PERSONNAGE][COOLDOWN_BOOST] = 0.0
def simuler_frame(etat, dt):
# On met a jour le cooldown a chaque frame
cooldowns_personnage(etat, dt)
gravite_personnage(etat, dt)
frottements_personnage(etat, dt)
moteur_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
Au cas où, le code complet du jeu se trouve dans le dépliant ci-dessous :
Code Complet
import pygame
from pygame import *
ECRAN = "ECRAN"
QUITTER = "QUITTER"
PERSONNAGE = "PERSONNAGE"
POSITION = "POSITION"
DIRECTION = "DIRECTION"
VITESSE_COURSE = "VITESSE_COURSE"
VITESSE = "VITESSE"
SAUT = "SAUT"
DEBUT = "DEBUT"
FIN = "FIN"
PLATEFORMES = "PLATEFORMES"
POSITION_DEBUT_FRAME = "POSITION_DEBUT_FRAME"
DASH = "DASH"
COEF_FROTTEMENT = "COEF_FROTTEMENT"
FORCE_MOTEUR = "FORCE_MOTEUR"
BOOST = "BOOST"
SAUTS_RESTANTS = "SAUTS_RESTANTS"
DASHS_RESTANTS = "DASH_RESTANTS"
COOLDOWN_BOOST = "COOLDOWN_BOOST"
def creer_personnage():
return {
POSITION : Vector2(1280/2, 720/2),
DIRECTION : Vector2(0, 0),
VITESSE_COURSE : 300,
VITESSE : Vector2(0, 0),
POSITION_DEBUT_FRAME : Vector2(0,0),
COEF_FROTTEMENT : 0.9,
FORCE_MOTEUR : 10000,
DASHS_RESTANTS : 1,
SAUTS_RESTANTS : 2,
COOLDOWN_BOOST : 0
}
def creer_plateforme(debut, fin, hauteur):
return {
DEBUT : Vector2(debut, hauteur),
FIN : Vector2(fin, hauteur)
}
def initialiser_niveau():
pygame.init()
ecran = pygame.display.set_mode([1280, 720])
return {
ECRAN : ecran,
PERSONNAGE : creer_personnage(),
PLATEFORMES : [
creer_plateforme(100, 300, 500),
creer_plateforme(100, 300, 200),
creer_plateforme(800, 900, 500),
creer_plateforme(1000, 1100, 400),
creer_plateforme(500, 700, 600),
]
}
def entrees_joueur():
quitter = False
saut = False
dash = False
boost = False
for evt in pygame.event.get():
if evt.type == pygame.KEYDOWN:
if evt.key == pygame.K_ESCAPE:
quitter = True
if evt.key == pygame.K_UP:
saut = True
if evt.key == pygame.K_LSHIFT:
dash = True
if evt.key == pygame.K_SPACE:
boost = True
touches = pygame.key.get_pressed()
direction = Vector2(0, 0)
if touches[pygame.K_RIGHT]:
direction.x += 1.0
if touches[pygame.K_LEFT]:
direction.x -= 1.0
if direction.length() > 0:
direction = direction.normalize()
return {
QUITTER : quitter,
DIRECTION : direction,
SAUT : saut,
DASH : dash,
BOOST : boost
}
def appliquer_entrees_joueur(etat, entrees):
etat[PERSONNAGE][DIRECTION] = entrees[DIRECTION]
if entrees[SAUT] and etat[PERSONNAGE][SAUTS_RESTANTS] > 0:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][VITESSE].x, -500)
etat[PERSONNAGE][SAUTS_RESTANTS] -= 1
if entrees[DASH] and etat[PERSONNAGE][DASHS_RESTANTS] > 0:
etat[PERSONNAGE][VITESSE] = Vector2(etat[PERSONNAGE][DIRECTION].x * 3000, etat[PERSONNAGE][VITESSE].y)
etat[PERSONNAGE][DASHS_RESTANTS] -= 1
if entrees[BOOST] and etat[PERSONNAGE][COOLDOWN_BOOST] <= 0:
etat[PERSONNAGE][VITESSE] = Vector2(0, etat[PERSONNAGE][VITESSE].y - 700)
etat[PERSONNAGE][COOLDOWN_BOOST] = 6.0
def gravite_personnage(etat, dt):
etat[PERSONNAGE][VITESSE] += Vector2(0, 1000) * dt
def frottements_personnage(etat, dt):
etat[PERSONNAGE][VITESSE].x = etat[PERSONNAGE][VITESSE].x * (1 - etat[PERSONNAGE][COEF_FROTTEMENT]) ** dt
if abs(etat[PERSONNAGE][VITESSE].x) < 1 :
etat[PERSONNAGE][VITESSE].x = 0
def moteur_personnage(etat, dt):
impulsion_max = etat[PERSONNAGE][FORCE_MOTEUR] * dt
vitesse_cible = etat[PERSONNAGE][DIRECTION].x * etat[PERSONNAGE][VITESSE_COURSE]
impulsion_ideale = vitesse_cible - etat[PERSONNAGE][VITESSE].x
if abs(impulsion_ideale) <= impulsion_max:
etat[PERSONNAGE][VITESSE].x += impulsion_ideale
else :
if impulsion_ideale < 0 :
impulsion_max = -impulsion_max
etat[PERSONNAGE][VITESSE].x += impulsion_max
def deplacer_personnage(etat, dt):
#L'impulsion est maintenant dans le moteur
etat[PERSONNAGE][POSITION_DEBUT_FRAME] = Vector2(etat[PERSONNAGE][POSITION])
etat[PERSONNAGE][POSITION] += (etat[PERSONNAGE][VITESSE]) * dt
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
etat[PERSONNAGE][POSITION].y = 720
etat[PERSONNAGE][VITESSE].y = 0
etat[PERSONNAGE][SAUTS_RESTANTS] = 2
etat[PERSONNAGE][DASHS_RESTANTS] = 1
def projection_segment(axe, debut, fin):
debut_proj = debut.project(axe)
d = debut_proj.x / axe.x if axe.x != 0 else debut_proj.y / axe.y
fin_proj = fin.project(axe)
f = fin_proj.x / axe.x if axe.x != 0 else fin_proj.y / axe.y
return (
min(d, f),
max(d, f)
)
def proj_superposees(axe, debut1, fin1, debut2, fin2):
p1 = projection_segment(axe, debut1, fin1)
p2 = projection_segment(axe, debut2, fin2)
return not (p1[1] < p2[0] or p2[1] < p1[0])
def vec_ortho(vec):
return vec.rotate(90).normalize()
def collision_plateforme(debut_p, fin_p, debut_m, fin_m):
if (debut_m - fin_m).length() == 0:
return False
deplacement = fin_m - debut_m
axes = [Vector2(0, 1), Vector2(1, 0), vec_ortho(deplacement)]
for axe in axes:
if not proj_superposees(axe, debut_p, fin_p, debut_m, fin_m):
return False
return True
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][VITESSE].y > 0:
for plateforme in etat[PLATEFORMES]:
if collision_plateforme(
plateforme[DEBUT],
plateforme[FIN],
etat[PERSONNAGE][POSITION_DEBUT_FRAME],
etat[PERSONNAGE][POSITION]
) :
etat[PERSONNAGE][POSITION].y = plateforme[DEBUT].y
etat[PERSONNAGE][VITESSE].y = 0
etat[PERSONNAGE][SAUTS_RESTANTS] = 2
etat[PERSONNAGE][DASHS_RESTANTS] = 1
def cooldowns_personnage(etat, dt):
if etat[PERSONNAGE][COOLDOWN_BOOST] > 0:
etat[PERSONNAGE][COOLDOWN_BOOST] -= dt
if etat[PERSONNAGE][COOLDOWN_BOOST] < 0:
etat[PERSONNAGE][COOLDOWN_BOOST] = 0.0
def simuler_frame(etat, dt):
cooldowns_personnage(etat, dt)
gravite_personnage(etat, dt)
frottements_personnage(etat, dt)
moteur_personnage(etat, dt)
deplacer_personnage(etat, dt)
collision_plateformes(etat, dt)
collision_sol(etat, dt)
def afficher_personnage(etat):
pygame.draw.circle(
etat[ECRAN],
(196, 154, 214),
etat[PERSONNAGE][POSITION] + Vector2(0, -20),
20
)
def afficher_plateformes(etat):
for plateforme in etat[PLATEFORMES]:
pygame.draw.rect(
etat[ECRAN],
(128, 128, 128),
Rect(
plateforme[DEBUT].x,
plateforme[DEBUT].y,
plateforme[FIN].x - plateforme[DEBUT].x,
10)
)
def afficher_niveau(etat):
etat[ECRAN].fill((0, 0, 0))
afficher_plateformes(etat)
afficher_personnage(etat)
pygame.display.flip()
def niveau():
etat = initialiser_niveau()
horloge = pygame.time.Clock()
stop = False
dt = 0.016
while not stop:
entrees = entrees_joueur()
if entrees[QUITTER] :
stop = True
appliquer_entrees_joueur(etat, entrees)
simuler_frame(etat, dt)
afficher_niveau(etat)
dt = horloge.tick(60) / 1000
pygame.quit()
niveau()
Il ne nous reste plus beaucoup de changements à faire avant d'avoir un jeu jouable :
- Donner un peu plus d'information au joueur/joueuse sur le cooldown et le nombre de sauts etc
- Permettre des niveaux verticalement plus grands
- Charger un niveau depuis un fichier.
Mais avant ça, il faut faire un peu de ménage !
Refactoring et redondance et magie
Globalement, le code a un défaut que l'on se traine depuis un moment, et qui risque de poser problème par la suite : la redondance.
La redondance dans le code, c'est quand plusieurs bouts de code remplissent un même rôle.
Activité
Essayez de repérer dans le code du jeu des éléments redondants :
- Deux fois la même valeur
- Deux bouts de code qui font la même chose
- ...
Et expliquez pourquoi ça pose problème.
Par exemple, dans la gestion du nombre de sauts. Pas le concept, la valeur 2 du nombre de sauts :
Elle apparaît à plusieurs endroits :
def creer_personnage():
return {
# ...
SAUTS_RESTANTS : 2,
}
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
#...
etat[PERSONNAGE][SAUTS_RESTANTS] = 2
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][VITESSE].y > 0:
for plateforme in etat[PLATEFORMES]:
if collision_plateforme(
# ...
) :
# ...
etat[PERSONNAGE][SAUTS_RESTANTS] = 2
Ici, la valeur est redondante, elle apparaît à plusieurs endroits.
Le problème que ce genre de redondance pose, c'est que si on veut modifier le nombre de sauts, on doit le modifier a plein d'endroits différents, et on a des risques d'oublier un endroit.
Proposez une manière de résoudre le problème présenté en solution de la partie 1 : on veut que la valeur du nombre de sauts soit à un seul endroit dans le code.
Faites le changement.
On a globalement deux choix :
Soit on stocke la valeur dans une variable (a vrai dire une constante) en haut du fichier.
Soit on stocke la valeur dans l'état du personnage.
Personellement, je choisis pour l'instant la première option, parce que ça laisse le code un peu plus libre.
# Je fais précéder de `CFG` pour "ConFiGuration"
CFG_MAX_SAUTS = 2
def creer_personnage():
return {
# ...
SAUTS_RESTANTS : CFG_MAX_SAUTS,
}
def collision_sol(etat, dt):
if etat[PERSONNAGE][POSITION].y > (720):
#...
etat[PERSONNAGE][SAUTS_RESTANTS] = CFG_MAX_SAUTS
def collision_plateformes(etat, dt):
if etat[PERSONNAGE][VITESSE].y > 0:
for plateforme in etat[PLATEFORMES]:
if collision_plateforme(
# ...
) :
# ...
etat[PERSONNAGE][SAUTS_RESTANTS] = CFG_MAX_SAUTS
D'une manière générale, c'est une bonne pratique de TOUJOURS ou presque mettre préférer utiliser des constantes ou variables plutôt que des valeurs littérales (c'est à dire écrites directement dans le code)
Ca permet d'éviter la redondance, mais aussi la création de valeurs magiques. Une valeur magique, c'est une valeur littérale qui a un sens obscur. Par exemple que représente 720 dans le code suivant ?
Réponse
la hauteur de la fenêtre en pixels !
C'était pas évident du tout à se rappeler.
A la place, on voudrait plutôt avoir :
if etat[PERSONNAGE][POSITION].y > (hauteur_fenetre):
# ou
if etat[PERSONNAGE][POSITION].y > (CFG_HAUTEUR_FENETRE):
Activité : attrapez les tous
Essayez de lister toutes les valeurs redondantes ou magiques dans le code, et faites en des constantes.
TODO CONTINUER ICI