Prévisualisation du niveau
La sélection de niveau est fonctionnelle mais très peu ergonomique : les noms des niveaux ne sont pas très parlants. On va donc faire en sorte que le niveau sélectionné soit affiché en arrière plan du menu.
Refactoring
On commence par un peu de refactoring. On modifie des fonctions de l'affichage du niveau pour pouvoir les réutiliser dans le menu. L'idée est de ne plus leur passer l'état en paramètre, mais directement ce qu'on veut afficher.
Par exemple
def rendre_personnage(etat):
ecran = etat.rendu.ecran
position = etat.simulation.personnage.position
pygame.draw.circle(ecran, COULEUR_PERSONNAGE, ref_sve(position), TAILLE_PERSONNAGE)
devient
def rendre_personnage(ecran, personnage):
position = personnage.position
pygame.draw.circle(ecran, COULEUR_PERSONNAGE, ref_sve(position), TAILLE_PERSONNAGE)
On fait de même pour les deux autres fonctions :
def rendre_projectiles(ecran, projectiles):
for p in projectiles:
position = p.position
pygame.draw.circle(ecran, COULEUR_PROJECTILES, ref_sve(position), p.taille)
def rendre_chronometre(ecran, decompte_victoire, caracteres):
chrono = f"{decompte_victoire:06.2f}" #Conversion en chaine
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:
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()
On adapte finalement la fonction d'affichage.
def afficher_rendu(etat, t):
ecran = etat.rendu.ecran
ecran.fill(COULEUR_ARRIERE_PLAN)
rendre_chronometre(etat.rendu.ecran, etat.simulation.decompte_victoire, etat.rendu.caracteres_chrono)
rendre_fin_de_partie(etat)
rendre_projectiles(etat.rendu.ecran, etat.simulation.projectiles)
rendre_personnage(etat.rendu.ecran, etat.simulation.personnage)
pygame.display.flip()
Chargement caractères menu
Dans la classe du Menu, on charge les caractères du chronomètres comme on l'avait fait pour la classe Rendu.
class Menu:
def __init__(self, niveaux, ecran):
# Ces 4 booléens sont mis a True
# si le joueur a lancé la commande dans la frame
self.haut = False
self.bas = False
self.lancer = False
self.quitter = False
self.niveaux = niveaux
#indice du niveau sélectionné.dans la liste des niveaux
self.niveau_selectionne = 0
self.ecran = ecran
self.textes_niveaux = {}
fonte = pygame.font.SysFont("Mono", TAILLE_MENU, True, False)
for n in niveaux:
self.textes_niveaux[n.nom] = fonte.render(n.nom, True, COULEUR_MENU)
fonte_chrono = pygame.font.SysFont("Mono", TAILLE_CHRONOMETRE, True, False)
self.caracteres_chrono = images_caracteres(fonte_chrono, "0123456789.", COULEUR_CHRONOMETRE)
On écrit une fonction rendre_niveau_menu
, qui effectue un rendu de la prévisualisation du niveau, en appelant les trois fonctions adaptées plus haut sur le niveau actuellement sélectionné.
def rendre_niveau_menu(menu):
ecran = menu.ecran
niveau = menu.niveaux[menu.niveau_selectionne]
rendre_chronometre(ecran, niveau.decompte_victoire, menu.caracteres_chrono)
rendre_projectiles(ecran, niveau.projectiles)
rendre_personnage(ecran, niveau.personnage)
Et on l'appelle dans la fonction d'affichage du menu, juste après avoir effacé l'écran.
def afficher_menu(menu):
ecran = menu.ecran
ecran.fill(COULEUR_ARRIERE_PLAN)
rendre_niveau_menu(menu)
n = menu.niveau_selectionne
rendre_nom_niveau(menu, n, 0, 255)
# On dessine les niveaux adjacents si besoin,
# en ajoutant un décalage de position
if n > 1:
rendre_nom_niveau(menu, n - 2, -ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)
if n > 0:
rendre_nom_niveau(menu, n - 1, -ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)
if n < len(menu.niveaux) - 1:
rendre_nom_niveau(menu, n + 1, ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)
if n < len(menu.niveaux) - 2:
rendre_nom_niveau(menu, n +2, ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)
pygame.display.flip()
Si on lance le jeu, on peut maintenant voir le niveau sélectionné en arrière plan dans le menu.
Le code complet à ce point 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)
## Menu
TAILLE_MENU = 50
COULEUR_MENU = (255, 255, 255)
ESPACEMENT_MENU = 120
GAIN_TRANSPARENCE_MENU = 100
####################
#### 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, nom, personnage, decompte_victoire, projectiles):
self.nom = nom
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(ecran, personnage):
position = personnage.position
pygame.draw.circle(ecran, COULEUR_PERSONNAGE, ref_sve(position), TAILLE_PERSONNAGE)
def rendre_projectiles(ecran, projectiles):
for p in projectiles:
position = p.position
pygame.draw.circle(ecran, COULEUR_PROJECTILES, ref_sve(position), p.taille)
def rendre_chronometre(ecran, decompte_victoire, caracteres):
chrono = f"{decompte_victoire:06.2f}" #Conversion en chaine
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:
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.rendu.ecran, etat.simulation.decompte_victoire, etat.rendu.caracteres_chrono)
rendre_fin_de_partie(etat)
rendre_projectiles(etat.rendu.ecran, etat.simulation.projectiles)
rendre_personnage(etat.rendu.ecran, etat.simulation.personnage)
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(nom):
with open(chemin_vers_niveau(nom), "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(nom, personnage, decompte_victoire, projectiles)
def lister_niveaux():
entrees = os.listdir(os.path.dirname(__file__))
fichiers_niveaux = [f for f in entrees if ".lasero" in f]
noms_niveaux = []
for f in fichiers_niveaux:
noms_niveaux.append(f.replace(".lasero", ""))
return noms_niveaux
def charger_niveaux():
noms_niveaux = lister_niveaux()
niveaux=[]
for n in noms_niveaux:
niveaux.append(charger_niveau(n))
return niveaux
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)
####################
#### MENU ####
####################
class Menu:
def __init__(self, niveaux, ecran):
# Ces 4 booléens sont mis a True
# si le joueur a lancé la commande dans la frame
self.haut = False
self.bas = False
self.lancer = False
self.quitter = False
self.niveaux = niveaux
#indice du niveau sélectionné.dans la liste des niveaux
self.niveau_selectionne = 0
self.ecran = ecran
self.textes_niveaux = {}
fonte = pygame.font.SysFont("Mono", TAILLE_MENU, True, False)
for n in niveaux:
self.textes_niveaux[n.nom] = fonte.render(n.nom, True, COULEUR_MENU)
fonte_chrono = pygame.font.SysFont("Mono", TAILLE_CHRONOMETRE, True, False)
self.caracteres_chrono = images_caracteres(fonte_chrono, "0123456789.", COULEUR_CHRONOMETRE)
def traiter_controles_menu(menu):
## on remet à zéro les contrôles pour ne pas reprendre
## l'état de la frame précédente
menu.haut = False
menu.bas = False
menu.lancer = False
menu.quitter = False
for e in pygame.event.get():
if e.type == pygame.KEYDOWN:
if e.key == TOUCHE_QUITTER:
menu.quitter = True
if e.key == TOUCHE_RECOMMENCER:
menu.lancer = True
if e.key == TOUCHE_HAUT:
menu.haut = True
if e.key == TOUCHE_BAS:
menu.bas = True
def selectionner_niveau_menu(menu):
if menu.haut:
menu.niveau_selectionne -= 1 #on remonte dans la liste
if menu.bas:
menu.niveau_selectionne += 1 #on descends dans la liste
#on vérifie qu'on ne sort pas de la liste
if menu.niveau_selectionne < 0:
menu.niveau_selectionne = 0
if menu.niveau_selectionne > len(menu.niveaux) - 1:
menu.niveau_selectionne = len(menu.niveaux) - 1
def lancer_niveau_menu(menu):
if menu.lancer:
jouer_niveau(menu.niveaux[menu.niveau_selectionne], menu.ecran)
def rendre_image_centree_decalee(ecran, image, decalage):
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 + decalage)
def rendre_nom_niveau(menu, i, decalage, transparence):
ecran = menu.ecran
texte = menu.textes_niveaux[menu.niveaux[i].nom]
texte.set_alpha(transparence) #on décide de la transparence
rendre_image_centree_decalee(ecran, texte, Vector2(0, decalage))
def rendre_niveau_menu(menu):
ecran = menu.ecran
niveau = menu.niveaux[menu.niveau_selectionne]
rendre_chronometre(ecran, niveau.decompte_victoire, menu.caracteres_chrono)
rendre_projectiles(ecran, niveau.projectiles)
rendre_personnage(ecran, niveau.personnage)
def afficher_menu(menu):
ecran = menu.ecran
ecran.fill(COULEUR_ARRIERE_PLAN)
rendre_niveau_menu(menu)
n = menu.niveau_selectionne
rendre_nom_niveau(menu, n, 0, 255)
# On dessine les niveaux adjacents si besoin,
# en ajoutant un décalage de position
if n > 1:
rendre_nom_niveau(menu, n - 2, -ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)
if n > 0:
rendre_nom_niveau(menu, n - 1, -ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)
if n < len(menu.niveaux) - 1:
rendre_nom_niveau(menu, n + 1, ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)
if n < len(menu.niveaux) - 2:
rendre_nom_niveau(menu, n +2, ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)
pygame.display.flip()
def lancer_menu(niveaux, ecran):
menu = Menu(niveaux, ecran)
clock = pygame.time.Clock()
while not menu.quitter:
clock.tick(FPS)
traiter_controles_menu(menu)
selectionner_niveau_menu(menu)
lancer_niveau_menu(menu)
afficher_menu(menu)
####################
#### MAIN ####
####################
def main():
pygame.init()
fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])
niveaux = charger_niveaux()
lancer_menu(niveaux, fenetre)
pygame.quit()
if __name__ == "__main__":
main()
Lisibilité
L'affichage de certains niveaux en arrière plan rends peu lisible le menu. Pour palier ce problème on va ajouter un "brouillard" gris partiellement transparent entre le menu et la pévisualisation du niveau, de manière à griser la prévisualisation.
On ne peut pas dessiner la transparence directement sur l'écran, a la place, on va la dessiner sur une image, à l'aide de la classe Surface
, et ensuite seulement la dessiner à l'écran.
Pour créer une surface, on passe à la méthode init les dimensions de la surface sous forme d'une tuple. Dans notre cas, ce sera les dimensions de la fenêtre.
On la remplit ensuite d'une couleur gris uni, renseignée dans une constante.
On lui donne une valeur de transparence, elle aussi renseignée dans une constante.
puis on la dessine à l'écran après avoir affiché la prévisualisation, et avant d'afficher le menu.
Pour éviter de recréer une nouvelle surface à chaque frame, on garde le brouillard comme un attribut de la classe Menu.
class Menu:
def __init__(self, niveaux, ecran):
# Ces 4 booléens sont mis a True
# si le joueur a lancé la commande dans la frame
self.haut = False
self.bas = False
self.lancer = False
self.quitter = False
self.niveaux = niveaux
#indice du niveau sélectionné.dans la liste des niveaux
self.niveau_selectionne = 0
self.ecran = ecran
self.textes_niveaux = {}
fonte = pygame.font.SysFont("Mono", TAILLE_MENU, True, False)
for n in niveaux:
self.textes_niveaux[n.nom] = fonte.render(n.nom, True, COULEUR_MENU)
fonte_chrono = pygame.font.SysFont("Mono", TAILLE_CHRONOMETRE, True, False)
self.caracteres_chrono = images_caracteres(fonte_chrono, "0123456789.", COULEUR_CHRONOMETRE)
self.brouillard = pygame.Surface((LARGEUR_FENETRE, HAUTEUR_FENETRE))
self.brouillard.fill(COULEUR_BROUILLARD_MENU)
self.brouillard.set_alpha(ALPHA_BROUILLARD_MENU)
Dans la fonction d'affichage du menu, on affiche simplement le brouillard.
def afficher_menu(menu):
ecran = menu.ecran
ecran.fill(COULEUR_ARRIERE_PLAN)
rendre_niveau_menu(menu)
ecran.blit(menu.brouillard, (0,0))
n = menu.niveau_selectionne
rendre_nom_niveau(menu, n, 0, 255)
# On dessine les niveaux adjacents si besoin,
# en ajoutant un décalage de position
if n > 1:
rendre_nom_niveau(menu, n - 2, -ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)
if n > 0:
rendre_nom_niveau(menu, n - 1, -ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)
if n < len(menu.niveaux) - 1:
rendre_nom_niveau(menu, n + 1, ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)
if n < len(menu.niveaux) - 2:
rendre_nom_niveau(menu, n +2, ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)
pygame.display.flip()
En lançant le jeu, on peut constater le brouillard devant le menu.
Le code complet de l'étape se trouve dans le dépliant ci-dessous.