Aller au contenu

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 :

structure

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 :

fenetre 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


# 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)
o
niveau()

Normalement, en lançant le code, vous verrez apparaître le cercle dans le coin haut gauche de la fenêtre :

point rose

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)


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 :

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
    # utiliser des - et des + au lieu d'une valeur 
    # en dur permet d'annuler le déplacement
    # si des flèches opposées sont pressées en même temps

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

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 #-1.0 parce que axe inversé
    if touches[pygame.K_DOWN]:
        direction.y -= -1.0

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)

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 ?

VITESSE : 10

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à.

        #dt pour *delta temps* ou "temps écoulé depuis la dernière frame"
        dt = horloge.tick(60)

        print(dt)

On lance le jeu, et on regarde l'affichage en console :

16
16
17
16
16
18

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

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 :

POSITION += DIRECTION * VITESSE

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 


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.

Il faut diminuer la valeur de la gravité :

etat[PERSONNAGE][INERTIE] += Vector2(0, 1000) * dt

Et éventuellement augmenter la valeur de l'impulsion du saut :

etat[PERSONNAGE][INERTIE] = Vector2(0, -5000) 

Perso je vais garder les valeurs initiales de 1000 pour la gravité et -5000 pour le saut

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 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"

def afficher_niveau(etat):
    etat[ECRAN].fill((0, 0, 0))

    pygame.draw.circle(
        etat[ECRAN],
        (196, 154, 214),
        etat[PERSONNAGE][POSITION] + Vector2(0, -20), #se retrouve ici
        20 #cette taille
    )

    pygame.display.flip()

Plateforme

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


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].y - etat[PLATEFORMES][FIN].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 


niveau()

Si vous lancez le jeu, vous verrez apparaître une plateforme :

plateforme

Maintenant, il faut gérer les collisions avec la plateforme.

Là, pas le choix, c'est DES MATHS.

TODO explications -> projections

TODO ajouter schema ici