Aller au contenu

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.

projectiles varies

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()