Aller au contenu

Fichier de niveau

La deuxième étape pour rendre notre jeu plus intéressant est d'augmenter drastiquement le nombre de projectiles. Mais rajouter des dizaines, voir des centaines, de projectiles dans le code le polluerait. Cela nous permettra aussi, par la suite, de rajouter des niveaux sans toucher au code du jeu.

Format du fichier

Un fichier de niveau, d'extension .lasero aura le format suivant :

1
2
3
4
(px,py)
temps_survie
(projx,projy) taille (dirx,diry) vitesse
...

La ligne 1 est la position initiale du personnage. px et py sont des flottants.

La ligne 2 est temps de survie attendu dans le niveau. temps_survie est également un flottant.

La ligne 3 est un projectile. projx et projy sont les coordonnées, flottantes, du projectile. taille est un flottant. dirx et diry sont la direction du projectile, deux flottants. On normalisera le vecteur au chargement du niveau, mais on essaiera de stocker uniquement des directions déjà normalisées. vitesse est un flottant.

Les lignes suivantes représentent d'autres projectiles, et sont au même format que la ligne 3.

On va supposer que les fichiers de niveaux seront toujours bien formattés, et donc qu'on aura pas à traiter d'erreur d'analyse.

Principe de l'analyse

TODO (prof) détailler un peu plus ici ?

Pour analyser le fichier, on le charge en mémoire, puis on analyse les lignes 1 et 2 pour en extraire la position du personnage et le temps de survie.

Ensuite, on analyse les lignes 3 et suivantes pour en extraire chaque projectile, que l'on ajoutera dans une liste. Finalement, on construit un objet niveau et on le retourne.

On peut déjà penser la structure de la fonction de chargement de niveau.

def charger_niveau(fichier):
    # on ouvre le fichier lecture ("r"),
    # on le manipulera à travers la variable f
    with open(fichier, "r") as f:
        # readlines charge le fichier en mémoire sous forme d'une liste 
        # de chaînes de caractères, chaque chaîne corresponds à une ligne du 
        # fichier.
        lignes = f.readlines() 

    position = analyse_personnage(lignes[0])
    decompte_victoire = analyse_decompte_victoire(lignes[1])
    projectiles = []

    # lignes[2:] est la liste ligne à laquelle 
    # on a enlevé les deux premiers éléments
    for l in lignes[2:]:
        projectiles.append(analyse_projectile(l))

    return Niveau(position, decompte_victoire, projectiles)

Il ne nous reste plus qu'à implémenter les fonctions analyse_personnage, analyse_decompte_victoire et analyse_projectile.

Analyse de la position

Analyser la position est relativement simple. On doit analyser une chaine de la forme (x,y) pour en faire un vecteur.

Pour celà, on se crée une fonction intermédiaire, analyse_vecteur, que l'on réutilisera pour les positions.

TODO (prof) ajouter schema

def analyse_vecteur(chaine):
    # replace(a,b) retourne une copie de la chaine où les caractères a ont 
    # étés remplacés par b
    # ici on s'en sert pour retirer les parenthèses
    sans_parentheses = chaine.replace("(", "").replace(")", "")
    # split partage une chaine en une list de plusieurs chaines selon un 
    # séparateur. "a,c".split(",") retourne ["a", "c"] 
    coords = sans_parentheses.split(",")
    # La première partie est x, la seconde y
    x = float(coords[0])
    y = float(coords[1])
    return Vector2(x, y)

On peut écrire notre fonction d'analyse de position du personnage:

def analyse_personnage(chaine):
    return Personnage(analyse_vecteur(chaine))

Analyse du décompte de victoire

Cette fonction est très simple à implanter :

def analyse_decompte_victoire(chaine):
    return float(chaine)

Analyse d'un projectile

L'analyse d'un projectile n'est pas bien plus complexe : on va séparer notre chaîne sur le caractère espace, puis analyser chaque morceau séparément.

TODO (prof) schema ici

def analyse_projectile(chaine):
    separee = chaine.split(" ")
    position = analyse_vecteur(separee[0])
    taille = float(separee[1])
    direction = analyse_vecteur(separee[2])
    vitesse = float(separee[3])
    return Projectile(position, taille, direction, vitesse)

Fichier à l'emplacement du jeu

On veut faire en sorte que le fichier de niveau chargé soit toujours pris dans le même répertoire que là où se trouve le fichier python du jeu.

Les manipulations avancées de fichiers sont fournies par le module os de Python. Le sous-module path fournis des fonctions qui vont nous intéresser ici.

On ajoute donc

import os

en haut de notre fichier.

En Python, le chemin vers le fichier dans lequel se trouve le code est contenu dans la variable __file__. On peut en extraire le chemin de répertoire avec la fonction dirname.

os.path.dirname("/un/chemin/vers/un/fichier") 

vaut

"/un/chemin/vers/un/"

Pour mettre bout à bout deux chemins de fichiers, il convient d'utiliser la fonction join, qui s'assure que le chemin corresponds bien au système d'exploitation sur lequel le programme s'execute. En effet, Linux utilise des / pour séparer les dossier dans le chemin, alors que Windows utilise des \.

On peut donc écrire une fonction qui prends le nom d'un niveau en paramètre, et retourne le chemin vers le fichier que l'on attends que le niveau ait.

def chemin_vers_niveau(niveau):
    repertoire = os.path.dirname(__file__)
    fichier = niveau + ".lasero" #on ajoute l'extension
    return os.path.join(repertoire, fichier)

Fonction main

Dans la fonction main, on va appeler le chargement de fichier sur un niveau dont le nom est spécifié dans une constante générale.

## Général 

NIVEAU = "niveau"
def main():
    pygame.init()

    fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])

    niveau = charger_niveau(chemin_vers_niveau(NIVEAU))

    jouer_niveau(niveau, fenetre)

    pygame.quit()

Avant d'appeler le programme, il nous faut créer un fichier de niveau.

Fichier de l'ancien niveau

Voici le fichier de niveau qui corresponds au niveau que nous avons joué dans l'étape précédente.

(640,360)
5.0
(200,300) 10 (1.0,1.0) 200
(400,120) 45 (1.0,0) 120
(800,600) 10 (-1.1,-0.3) 300
(200,600) 15 (0.2,-0.1) 250

Copiez-collez le dans un ficher niveau.lasero à côté de votre fichier Python.

En lançant le programme, vous devriez retrouver le même niveau que précédemment.

Le code complet de l'étape se trouve dans le dépliant ci-dessous :

Code Complet
import pygame
from pygame.math import Vector2
import os

####################
####   CONFIG   ####
####################

## Général 

NIVEAU = "niveau"

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
TOUCHE_RECOMMENCER = pygame.K_r

## Simulation

VITESSE_PERSONNAGE = 300
TAILLE_PERSONNAGE = 10
DECOMPTE_VICTOIRE = 5.0

## Rendu

COULEUR_ARRIERE_PLAN = (0, 0, 0)
COULEUR_PERSONNAGE = (255, 146, 205)
COULEUR_PROJECTILES = (146, 255, 205)
COULEUR_CHRONOMETRE = (150, 150, 170)
TAILLE_CHRONOMETRE = 30
MARGES_CHRONOMETRE = 20
TAILLE_FIN_DE_PARTIE = 150
COULEUR_FIN_DE_PARTIE = (150, 150, 170)

####################
####  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)
        self.recommencer = False

def initialiser_controles(etat):
    etat.controles = Controles()

def reinitialiser_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
                if e.key == TOUCHE_RECOMMENCER:
                    etat.controles.recommencer = 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

def copier_personnage(personnage):
    return Personnage(personnage.position.copy())

class Projectile: 

    def __init__(self, position, taille, direction, vitesse):
        self.position = position
        self.direction = direction
        self.taille = taille
        if self.direction.length != 0:
            self.direction.normalize_ip()
        self.vitesse = vitesse

def copier_projectile(projectile):
    return Projectile(projectile.position.copy(), projectile.taille, projectile.direction.copy(), projectile.vitesse)

def copier_projectiles(projectiles):
    copie = []
    for p in projectiles:
        copie.append(copier_projectile(p))
    return copie

class Niveau:
    def __init__(self, personnage, decompte_victoire, projectiles):
        self.personnage = personnage
        self.decompte_victoire = decompte_victoire
        self.projectiles = projectiles

class Simulation:

    def __init__(self, niveau):
        self.personnage = copier_personnage(niveau.personnage)
        self.projectiles = copier_projectiles(niveau.projectiles)
        self.collision = False
        self.decompte_victoire = niveau.decompte_victoire
        self.attente = True

def initialiser_simulation(etat, niveau):
    etat.simulation = Simulation(niveau)

def reinitialiser_simulation(etat, niveau):
    initialiser_simulation(etat, niveau)

def intersection(c1, r1, c2, r2):
    return c1.distance_to(c2) <= r1 + r2

def detecter_collisions(etat):
    perso = etat.simulation.personnage
    for p in etat.simulation.projectiles:
        if intersection(perso.position, TAILLE_PERSONNAGE, p.position, p.taille):
            etat.simulation.collision = True

def deplacer_personnage(etat, t):
    direction = etat.controles.direction
    etat.simulation.personnage.position += direction * VITESSE_PERSONNAGE * t

def limiter_deplacement_personnage(etat):
    p = etat.simulation.personnage
    if p.position.x < TAILLE_PERSONNAGE:
        p.position.x = TAILLE_PERSONNAGE
    if p.position.x > LARGEUR_FENETRE - TAILLE_PERSONNAGE:
        p.position.x = LARGEUR_FENETRE - TAILLE_PERSONNAGE
    if p.position.y < TAILLE_PERSONNAGE:
        p.position.y = TAILLE_PERSONNAGE
    if p.position.y > HAUTEUR_FENETRE - TAILLE_PERSONNAGE:
        p.position.y = HAUTEUR_FENETRE - TAILLE_PERSONNAGE

def deplacer_projectiles(etat, t):
    for p in etat.simulation.projectiles:
        p.position += p.direction * p.vitesse * t

def faire_rebondir_projectiles(etat):
    for p in etat.simulation.projectiles:
        if p.position.x < p.taille:
            p.position.x = p.taille
            p.direction.x *= -1
        if p.position.x > LARGEUR_FENETRE - p.taille:
            p.position.x = LARGEUR_FENETRE - p.taille
            p.direction.x *= -1
        if p.position.y < p.taille:
            p.position.y = p.taille
            p.direction.y *= -1
        if p.position.y > HAUTEUR_FENETRE - p.taille:
            p.position.y = HAUTEUR_FENETRE - p.taille
            p.direction.y *= -1

def decompter_victoire(etat, t):
    etat.simulation.decompte_victoire -= t
    if etat.simulation.decompte_victoire < 0:
        etat.simulation.decompte_victoire = 0.0

def verifier_attente(etat):
    if etat.simulation.attente and etat.controles.direction.length() != 0.0:
        etat.simulation.attente = False

def avancer_simulation(etat, t):
    if etat.simulation.attente:
        verifier_attente(etat)
    elif not etat.simulation.collision and etat.simulation.decompte_victoire != 0.0:
        deplacer_personnage(etat, t)
        limiter_deplacement_personnage(etat)
        deplacer_projectiles(etat, t)
        faire_rebondir_projectiles(etat)
        detecter_collisions(etat)
        decompter_victoire(etat, t)

####################
####   RENDU    ####
####################

def images_caracteres(fonte, caracteres, couleur):
    cars = {}
    for c in caracteres: 
        cars[c] = fonte.render(c, True, couleur)
    return cars

class Rendu:
    def __init__(self, ecran):
        self.ecran = ecran

        fonte_chrono = pygame.font.SysFont("Mono", TAILLE_CHRONOMETRE, True, False)
        self.caracteres_chrono = images_caracteres(fonte_chrono, "0123456789.", COULEUR_CHRONOMETRE)

        fonte_fin = pygame.font.SysFont("Mono", TAILLE_FIN_DE_PARTIE, True, False)
        self.texte_victoire = fonte_fin.render("VICTOIRE", True, COULEUR_FIN_DE_PARTIE)
        self.texte_defaite = fonte_fin.render("DEFAITE", True, COULEUR_FIN_DE_PARTIE)

def ref_sve(v):
    return Vector2(v.x, HAUTEUR_FENETRE - v.y)

def initialiser_rendu(etat, ecran):
    etat.rendu = Rendu(ecran)

def reinitialiser_rendu(etat):
    pass

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 rendre_projectiles(etat):
    ecran = etat.rendu.ecran
    for p in etat.simulation.projectiles:
        position = p.position
        pygame.draw.circle(ecran, COULEUR_PROJECTILES, ref_sve(position), p.taille)

def rendre_chronometre(etat):
    chrono = f"{etat.simulation.decompte_victoire:06.2f}" #Conversion en chaine
    caracteres = etat.rendu.caracteres_chrono

    x = MARGES_CHRONOMETRE #x de départ (référentiel écran)
    y = MARGES_CHRONOMETRE #y de départ (référentiel écran)

    for c in chrono:
        etat.rendu.ecran.blit(caracteres[c], (x, y))
        #la methode get_width de l'objet image renvoie la largeur de l'image.
        x += caracteres[c].get_width()

def rendre_image_centree(ecran, image):
    centre_ecran = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2) 
    centre_image = Vector2(image.get_width()/2, image.get_height()/2)
    ecran.blit(image, centre_ecran - centre_image)

def rendre_fin_de_partie(etat):
    ecran = etat.rendu.ecran
    if etat.simulation.collision : 
        #si on a perdu
        rendre_image_centree(ecran, etat.rendu.texte_defaite)
    elif etat.simulation.decompte_victoire == 0.0:
        #si la partie est finie et qu'on a pas perdu
        rendre_image_centree(ecran, etat.rendu.texte_victoire)

def afficher_rendu(etat, t):
    ecran = etat.rendu.ecran
    ecran.fill(COULEUR_ARRIERE_PLAN)

    rendre_chronometre(etat)

    rendre_fin_de_partie(etat)

    rendre_projectiles(etat)

    rendre_personnage(etat)

    pygame.display.flip()  


####################
####   NOYAU    ####
####################

def chemin_vers_niveau(niveau):
    repertoire = os.path.dirname(__file__)
    fichier = niveau + ".lasero" #on ajoute l'extension
    return os.path.join(repertoire, fichier)

def analyse_vecteur(chaine):
    sans_parentheses = chaine.replace("(", "").replace(")", "")
    coords = sans_parentheses.split(",")
    x = float(coords[0])
    y = float(coords[1])
    return Vector2(x, y)

def analyse_personnage(chaine):
    return Personnage(analyse_vecteur(chaine))

def analyse_decompte_victoire(chaine):
    return float(chaine)

def analyse_projectile(chaine):
    separee = chaine.split(" ")
    position = analyse_vecteur(separee[0])
    taille = float(separee[1])
    direction = analyse_vecteur(separee[2])
    vitesse = float(separee[3])
    return Projectile(position, taille, direction, vitesse)

def charger_niveau(fichier):

    with open(fichier, "r") as f:
        lignes = f.readlines() 

    personnage = analyse_personnage(lignes[0])
    decompte_victoire = analyse_decompte_victoire(lignes[1])
    projectiles = []

    for l in lignes[2:]:
        projectiles.append(analyse_projectile(l))

    return Niveau(personnage, decompte_victoire, projectiles)

def jouer_niveau(niveau, ecran):
    etat = GameState()

    initialiser_controles(etat)
    initialiser_simulation(etat, niveau)
    initialiser_rendu(etat, ecran)

    clock = pygame.time.Clock()

    while not etat.controles.quitter:

        if etat.controles.recommencer:
            reinitialiser_controles(etat)
            reinitialiser_simulation(etat, niveau)
            reinitialiser_rendu(etat)

        t = clock.tick(FPS)/1000

        traiter_controles(etat, t)

        avancer_simulation(etat,t)

        afficher_rendu(etat, t)

####################
####    MAIN    ####
####################

def main():
    pygame.init()

    fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])

    niveau = charger_niveau(chemin_vers_niveau(NIVEAU))

    jouer_niveau(niveau, fenetre)

    pygame.quit()

if __name__ == "__main__":
    main()

Nouveau niveau

Pour pimenter les choses, je vous propose un niveau un peu plus complexe. Il m'a fallu quelques essais pour arriver à la victoire.

Copiez-collez le dans le fichier niveau.lasero si vous voulez le tester.

(640,360)
20
(810.0,360.0) 20.0 (1.0,0.0) 200
(807.9070179011734,386.5938590568392) 18.090169943749473 (0.9876883405951378,0.15643446504023073) 200
(801.6796077701761,412.5328890437411) 13.090169943749475 (0.9510565162951536,0.3090169943749476) 200
(791.4711091120225,437.17838495572295) 13.090169943749473 (0.8910065241883678,0.45399049973954675) 200
(777.5328890437411,459.9234928897204) 18.090169943749473 (0.8090169943749476,0.587785252292473) 200
(760.2081528017131,480.2081528017131) 20.0 (0.7071067811865477,0.7071067811865474) 200
(739.9234928897205,497.5328890437411) 18.090169943749476 (0.5877852522924732,0.8090169943749472) 200
(717.178384955723,511.47110911202253) 13.090169943749476 (0.453990499739547,0.8910065241883677) 200
(692.5328890437411,521.6796077701761) 13.090169943749473 (0.3090169943749476,0.9510565162951536) 200
(666.5938590568393,527.9070179011734) 18.090169943749473 (0.15643446504023106,0.9876883405951378) 200
(640.0,530.0) 20.0 (0.0,1.0) 200
(613.4061409431607,527.9070179011734) 18.090169943749476 (-0.15643446504023106,0.9876883405951378) 200
(587.4671109562589,521.6796077701761) 13.090169943749476 (-0.3090169943749476,0.9510565162951536) 200
(562.8216150442771,511.47110911202253) 13.090169943749471 (-0.45399049973954647,0.891006524188368) 200
(540.0765071102795,497.5328890437411) 18.090169943749473 (-0.5877852522924732,0.8090169943749472) 200
(519.791847198287,480.2081528017131) 20.0 (-0.7071067811865474,0.7071067811865477) 200
(502.4671109562589,459.9234928897205) 18.090169943749476 (-0.8090169943749472,0.5877852522924732) 200
(488.52889088797747,437.17838495572295) 13.090169943749478 (-0.8910065241883678,0.45399049973954675) 200
(478.32039222982394,412.5328890437411) 13.09016994374947 (-0.9510565162951536,0.3090169943749476) 200
(472.0929820988266,386.5938590568393) 18.090169943749473 (-0.9876883405951378,0.15643446504023106) 200
(470.0,360.0) 20.0 (-1.0,0.0) 200
(472.0929820988266,333.4061409431608) 18.090169943749476 (-0.9876883405951378,-0.15643446504023073) 200
(478.3203922298239,307.467110956259) 13.09016994374948 (-0.9510565162951536,-0.3090169943749472) 200
(488.52889088797747,282.82161504427705) 13.09016994374947 (-0.8910065241883678,-0.45399049973954675) 200
(502.4671109562589,260.0765071102796) 18.090169943749473 (-0.8090169943749476,-0.587785252292473) 200
(519.7918471982869,239.79184719828692) 20.0 (-0.7071067811865477,-0.7071067811865474) 200
(540.0765071102795,222.46711095625895) 18.09016994374948 (-0.5877852522924734,-0.8090169943749473) 200
(562.821615044277,208.52889088797747) 13.09016994374948 (-0.453990499739547,-0.8910065241883677) 200
(587.4671109562589,198.3203922298239) 13.090169943749467 (-0.3090169943749475,-0.9510565162951534) 200
(613.4061409431607,192.09298209882658) 18.09016994374947 (-0.15643446504023104,-0.9876883405951378) 200
(640.0,190.0) 20.0 (0.0,-1.0) 200
(666.5938590568392,192.09298209882658) 18.09016994374948 (0.15643446504023037,-0.9876883405951378) 200
(692.5328890437411,198.32039222982388) 13.090169943749482 (0.3090169943749475,-0.9510565162951536) 200
(717.1783849557229,208.52889088797744) 13.090169943749466 (0.4539904997395464,-0.891006524188368) 200
(739.9234928897204,222.46711095625892) 18.09016994374947 (0.5877852522924728,-0.8090169943749477) 200
(760.208152801713,239.7918471982869) 20.0 (0.7071067811865474,-0.7071067811865478) 200
(777.5328890437411,260.0765071102795) 18.09016994374948 (0.8090169943749472,-0.5877852522924732) 200
(791.4711091120225,282.821615044277) 13.090169943749483 (0.8910065241883677,-0.453990499739547) 200
(801.6796077701761,307.4671109562589) 13.090169943749466 (0.9510565162951536,-0.3090169943749476) 200
(807.9070179011734,333.4061409431607) 18.09016994374947 (0.9876883405951378,-0.15643446504023106) 200

TODO (prof) expliquer la generation ?