Aller au contenu

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.

On change la valeur de la constante FPS

FPS = 120 #ou FPS = 30

On observe alors que pour 30 FPS, le personnage va plus lentement, et pour 120, le personnage va plus vite.

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.

VITESSE_PERSONNAGE = 300

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()