Projectiles variés
Jusqu'à présent, notre "niveau" ne comporte que 4 projectiles, qui sont tous identiques, et vont tous à la même vitesse.
Pour le rendre plus amusant, on peut dans un premier temps faire trois améliorations :
- Permettre des projectiles avec des tailles et des vitesses variées, ce qui est l'objet de cette étape
- Ajouter des projectiles de manière à ce que l'esquive devienne plus difficile, ce qui sera l'objet de l'etape suivante.
- Permettre de choisir un niveau parmi plusieurs, ce qui sera fait dans à l'étape encore après.
Classe projectile
On commence par remplacer les constantes TAILLE_PROJECTILES
et VITESSE_PROJECTILES
par des attributs dans la classe Projectile
.
On les ajoute à la classe, et on supprime les constantes du code.
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
Simulation
Dans la simulation, il nous faut tenir compte à plusieurs endroits de ces nouveaux attributs. Là où l'on utilisait les deux constantes, on va utiliser les attributs à la place.
Dans la fonction copier_projectile
:
def copier_projectile(projectile):
return Projectile(projectile.position.copy(), projectile.taille, projectile.direction.copy(), projectile.vitesse)
Dans la fonction detecter_collisions
:
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
Dans la fonction deplacer_projectiles
:
def deplacer_projectiles(etat, t):
for p in etat.simulation.projectiles:
p.position += p.direction * p.vitesse * t
Et enfin, dans la fonction faire_rebondir_projectiles
:
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
Rendu
De la même manière, on doit adapter la fonction de rendu des projectiles pour qu'elle utilise les attributs :
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)
Fonction main
Finalement, modifie la fonction main
pour spécifier les nouveaux attributs au moment de la création.
niveau = Niveau(
Personnage(Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2)),
DECOMPTE_VICTOIRE,
[
Projectile(Vector2(200, 300), 10, Vector2(1.0, 1.0), 200),
Projectile(Vector2(400, 120), 45, Vector2(1.0, 0), 120),
Projectile(Vector2(800, 600), 10, Vector2(-1.1, -0.3), 300),
Projectile(Vector2(200, 600), 15, Vector2(0.2, -0.1), 250),
]
)
Si on lance le jeu, on peut voir la variété des projectiles.
Le code complet de l'étape est dans le dépliant ci dessous :
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
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 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 = Niveau(
Personnage(Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2)),
DECOMPTE_VICTOIRE,
[
Projectile(Vector2(200, 300), 10, Vector2(1.0, 1.0), 200),
Projectile(Vector2(400, 120), 45, Vector2(1.0, 0), 120),
Projectile(Vector2(800, 600), 10, Vector2(-1.1, -0.3), 300),
Projectile(Vector2(200, 600), 15, Vector2(0.2, -0.1), 250),
]
)
jouer_niveau(niveau, fenetre)
pygame.quit()
if __name__ == "__main__":
main()