Fluidité
TODO (prof) mieux expliquer l'intérêt ?
FPS variables
Exercice
A quelle vitesse se déplace le personnage, en pixels par seconde ?
Regardez la vitesse du personnage, qui vaut 5.
Et aussi les FPS, qui valent 60.
Chaque frame, notre personnage se déplace de 5 pixels. Il y a 60 frames par seconde.
notre personnage se déplace donc de 300 pixels par secondes.
Changez le nombre de FPS du jeu. Par exemple 30, puis 120.
Que remarquez-vous ?
Modifiez simplement la valeur de la constante FPS
.
Pourquoi une influence des FPS sur la vitesse du personnage peut poser problème ?
Si on modifie le nombre de FPS du jeu, celà change le gameplay, qui devient plus rapide ou plus lent. Pourtant, il faut que notre jeu puisse tourner à différentes FPS, pour s'adapter au matériel et aux exigences des joueurs.
Pire encore, on ne pourra pas toujours garantir que les FPS seront parfaitement stables, si par exemple d'autres logiciels tournent sur la machine et mobilisent le CPU de manière intensive. Et dans ce cas là, la vitesse du personnage serait également instable, rendant le jeu totalement injouable.
Pour rendre l'évolution du monde indépendante des FPS, on va mesurer l'écoulement du temps dans notre jeu, et non l'enchaînement des frames.
Nos fonctions de systèmes exécutées dans la boucle principale recevront le temps écoulé depuis la dernière frame, en secondes.
def traiter_controles(etat, t):
...
def avancer_simulation(etat, t):
...
def afficher_rendu(etat, t):
...
Ca tombe bien, la méthode tick
de la classe time.Clock
, que nous utilisons déjà, renvoie les milisecondes écoulées depuis le dernier appel à tick
. Il nous suffit de convertir en secondes en divisant par 1000 :
def main():
pygame.init()
etat = GameState()
fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])
initialiser_controles(etat)
initialiser_simulation(etat)
initialiser_rendu(etat, fenetre)
clock = pygame.time.Clock()
while not etat.controles.quitter:
t = clock.tick(FPS)/1000
traiter_controles(etat, t)
avancer_simulation(etat, t)
afficher_rendu(etat, t)
pygame.quit()
Pour le moment, la seule fonction qui se base sur le temps est avancer_simulation
. Pour prendre en compte t
, il nous suffit de multiplier la vitesse par t
pour obtenir une distance avant de l'ajouter à la position du personnage, on ne fait qu'appliquer les lois de la physique !
def avancer_simulation(etat, t):
direction = etat.controles.direction
etat.simulation.personnage.position += direction * VITESSE_PERSONNAGE * t
Il nous faut aussi modifier la vitesse du personnage, qui est désormais en pixels par seconde et non plus en pixel par frame.
Le dépliant suivant contient le code complet à ce point :
Code Complet
import pygame
from pygame.math import Vector2
####################
#### CONFIG ####
####################
## Général
FPS = 60
LARGEUR_FENETRE = 1280
HAUTEUR_FENETRE = 720
## Controles
TOUCHE_QUITTER = pygame.K_ESCAPE
TOUCHE_HAUT = pygame.K_UP
TOUCHE_BAS = pygame.K_DOWN
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
## Simulation
VITESSE_PERSONNAGE = 300
TAILLE_PERSONNAGE = 10
## Rendu
COULEUR_ARRIERE_PLAN = (0, 0, 0)
COULEUR_PERSONNAGE = (255, 146, 205)
####################
#### GENERAL ####
####################
class GameState :
def __init__(self):
self.controles = None # on met a None parce que ces attributs seront initialisés plus tard
self.simulation = None
self.rendu = None
####################
#### CONTROLEUR ####
####################
class Controles :
def __init__(self):
self.quitter = False
self.direction = Vector2(0,0)
def initialiser_controles(etat):
etat.controles = Controles()
def traiter_controles(etat, t):
for e in pygame.event.get():
if e.type == pygame.KEYDOWN:
if e.key == TOUCHE_QUITTER:
etat.controles.quitter = True
direction = Vector2(0, 0)
clavier = pygame.key.get_pressed()
if clavier[TOUCHE_HAUT]:
direction.y += 1.0
if clavier[TOUCHE_BAS]:
direction.y -= 1.0
if clavier[TOUCHE_DROITE]:
direction.x += 1.0
if clavier[TOUCHE_GAUCHE]:
direction.x -= 1.0
etat.controles.direction = direction
####################
#### SIMULATION ####
####################
class Personnage:
def __init__(self, position):
self.position = position
class Simulation:
def __init__(self, personnage):
self.personnage = personnage
def initialiser_simulation(etat):
pos = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2)
p = Personnage(pos)
etat.simulation = Simulation(p)
def avancer_simulation(etat, t):
direction = etat.controles.direction
etat.simulation.personnage.position += direction * VITESSE_PERSONNAGE * t
####################
#### RENDU ####
####################
class Rendu:
def __init__(self, ecran):
self.ecran = ecran
def ref_sve(v):
return Vector2(v.x, HAUTEUR_FENETRE - v.y)
def initialiser_rendu(etat, ecran):
etat.rendu = Rendu(ecran)
def rendre_personnage(etat):
ecran = etat.rendu.ecran
position = etat.simulation.personnage.position
pygame.draw.circle(ecran, COULEUR_PERSONNAGE, ref_sve(position), TAILLE_PERSONNAGE)
def afficher_rendu(etat, t):
ecran = etat.rendu.ecran
ecran.fill(COULEUR_ARRIERE_PLAN)
rendre_personnage(etat)
pygame.display.flip()
####################
#### MAIN ####
####################
def main():
pygame.init()
etat = GameState()
fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])
initialiser_controles(etat)
initialiser_simulation(etat)
initialiser_rendu(etat, fenetre)
clock = pygame.time.Clock()
while not etat.controles.quitter:
t = clock.tick(FPS)/1000
traiter_controles(etat, t)
avancer_simulation(etat, t)
afficher_rendu(etat, t)
pygame.quit()
if __name__ == "__main__":
main()
Si on l'execute, peu importe le nombre de FPS, le personnage aura toujours la même vitesse à l'écran !
Vitesse diagonale
Il reste un problème : le personnage se déplace plus vite en diagonale qu'à l'horizontale ou à la verticale.
Celà vient du fait que quand on va à l'horizontale, par exemple direction (1,0)
, le vecteur direction a pour longueur 1, alors que quand on va en diagonale, par exemple (1, -1)
, le vecteur direction a pour longueur environ 1.41
TODO schema ici
Pour corriger ça, on va normaliser le vecteur direction, c'est à dire le transformer pour que sa longueur soit 1 sans changer sa direction. La classe Vector2
nous propose déjà un tel outils, avec la méthode normalize_ip
.
TODO schema ici ?
Voici un exemple minimaliste d'utilisation :
v = pygame.math.Vector2(1, -1)
print(v)
print(v.length()) #la methode length permet de connaitre la longueur du vecteur
v.normalize_ip() #on met sa longueur à 1
print(v2)
print(v2.length())
Cette méthode ne marche pas pour un vecteur de longueur 0. On va également devoir en tenir compte dans notre code.
Modifions donc la fonction traiter_controles
pour normaliser le vecteur si sa longueur n'est pas 0 :
def traiter_controles(etat, t):
for e in pygame.event.get():
if e.type == pygame.KEYDOWN:
if e.key == TOUCHE_QUITTER:
etat.controles.quitter = True
direction = Vector2(0, 0)
clavier = pygame.key.get_pressed()
if clavier[TOUCHE_HAUT]:
direction.y += 1.0
if clavier[TOUCHE_BAS]:
direction.y -= 1.0
if clavier[TOUCHE_DROITE]:
direction.x += 1.0
if clavier[TOUCHE_GAUCHE]:
direction.x -= 1.0
if direction.length() != 0:
direction.normalize_ip()
etat.controles.direction = direction
Voici le code complet dans un dépliant :
Code Complet
import pygame
from pygame.math import Vector2
####################
#### CONFIG ####
####################
## Général
FPS = 60
LARGEUR_FENETRE = 1280
HAUTEUR_FENETRE = 720
## Controles
TOUCHE_QUITTER = pygame.K_ESCAPE
TOUCHE_HAUT = pygame.K_UP
TOUCHE_BAS = pygame.K_DOWN
TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
## Simulation
VITESSE_PERSONNAGE = 300
TAILLE_PERSONNAGE = 10
## Rendu
COULEUR_ARRIERE_PLAN = (0, 0, 0)
COULEUR_PERSONNAGE = (255, 146, 205)
####################
#### GENERAL ####
####################
class GameState :
def __init__(self):
self.controles = None # on met a None parce que ces attributs seront initialisés plus tard
self.simulation = None
self.rendu = None
####################
#### CONTROLEUR ####
####################
class Controles :
def __init__(self):
self.quitter = False
self.direction = Vector2(0,0)
def initialiser_controles(etat):
etat.controles = Controles()
def traiter_controles(etat, t):
for e in pygame.event.get():
if e.type == pygame.KEYDOWN:
if e.key == TOUCHE_QUITTER:
etat.controles.quitter = True
direction = Vector2(0, 0)
clavier = pygame.key.get_pressed()
if clavier[TOUCHE_HAUT]:
direction.y += 1.0
if clavier[TOUCHE_BAS]:
direction.y -= 1.0
if clavier[TOUCHE_DROITE]:
direction.x += 1.0
if clavier[TOUCHE_GAUCHE]:
direction.x -= 1.0
if direction.length() != 0:
direction.normalize_ip()
etat.controles.direction = direction
####################
#### SIMULATION ####
####################
class Personnage:
def __init__(self, position):
self.position = position
class Simulation:
def __init__(self, personnage):
self.personnage = personnage
def initialiser_simulation(etat):
pos = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2)
p = Personnage(pos)
etat.simulation = Simulation(p)
def avancer_simulation(etat, t):
direction = etat.controles.direction
etat.simulation.personnage.position += direction * VITESSE_PERSONNAGE * t
####################
#### RENDU ####
####################
class Rendu:
def __init__(self, ecran):
self.ecran = ecran
def ref_sve(v):
return Vector2(v.x, HAUTEUR_FENETRE - v.y)
def initialiser_rendu(etat, ecran):
etat.rendu = Rendu(ecran)
def rendre_personnage(etat):
ecran = etat.rendu.ecran
position = etat.simulation.personnage.position
pygame.draw.circle(ecran, COULEUR_PERSONNAGE, ref_sve(position), TAILLE_PERSONNAGE)
def afficher_rendu(etat, t):
ecran = etat.rendu.ecran
ecran.fill(COULEUR_ARRIERE_PLAN)
rendre_personnage(etat)
pygame.display.flip()
####################
#### MAIN ####
####################
def main():
pygame.init()
etat = GameState()
fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])
initialiser_controles(etat)
initialiser_simulation(etat)
initialiser_rendu(etat, fenetre)
clock = pygame.time.Clock()
while not etat.controles.quitter:
t = clock.tick(FPS)/1000
traiter_controles(etat, t)
avancer_simulation(etat, t)
afficher_rendu(etat, t)
pygame.quit()
if __name__ == "__main__":
main()