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 :
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:
Analyse du décompte de victoire
Cette fonction est très simple à implanter :
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
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
.
vaut
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.
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 ?