Aller au contenu

Collisions

On veut détecter une collision (c'est à dire un contact) entre le personnage et un projectile, et arrêter la simulation dans ce cas.

Idée mathématique

Les projectiles et le personnages sont des cercles, ou plutôt, des disques. Cette forme n'a pas été choisie au hasard : de par sa simplicité, elle facilite la détection de collisions.

D'un point de vue mathématique, celà revient à savoir si les deux disques ont une intersection, c'est à dire qu'il existe au moins un point du plan qui appartient aux deux disques.

TODO (prof) Illustration ici

Comme on ne cherche pas à savoir comment est cette intersection, mais seulement si elle existe, il existe une méthode très simple :

Deux disques \(a\) et \(b\), de centres respectifs \(C_a\) et \(C_b\) et rayons respectifs \(r_a\) et \(r_b\), ont un intersection si la distance entre \(C_a\) et \(C_b\) est inférieure ou égale à \(r_a + r_b\).

TODO (prof) Illustration ici

Simulation

C'est l'heure de mettre ça en application. Dans la classe Simulation, on va rajouter un attribut collision, un booléen qui dit si une collision entre le personnage et un projectile a eu lieu dans la partie.

class Simulation:

    def __init__(self, personnage):
        self.personnage = personnage
        self.projectiles = [
            Projectile(Vector2(200, 300)),
            Projectile(Vector2(400, 120)),
            Projectile(Vector2(800, 600)),
            Projectile(Vector2(200, 600)),
        ]
        self.collision = False

On doit alors écrire une fonction intersection, qui prends en paramètres deux rayons et deux centres, et retourne True si les deux disques correspondants ont une intersection, False sinon.

Pour calculer la distance entre deux Vector2, on peut utiliser la méthode distance_to.

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

Utilisons cette fonctions pour écrire une fonction de simulation qui détecte les collisions. L'idée est de tester chaque projectile avec le joueur.

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

On doit appeler cette fonction dans la simulation, après avoir déplacé le personnage.

On va en profiter pour déplacer le code de déplacement du personnage dans une fonction, pour plus de lisibilité.

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

Boucle principale

Une fois qu'une collision a été détectée, on doit arrêter la simulation. L'idée est de continuer à afficher l'état du jeu, mais de ne plus le faire évoluer pour que le joueur voit là où il a eu une collision, avant de quitter le jeu avec ECHAP.

Ceci se fait dans la boucle principale.

while not etat.controles.quitter:

        t = clock.tick(FPS)/1000

        traiter_controles(etat, t)

        if not etat.simulation.collision:
            avancer_simulation(etat, t)

        afficher_rendu(etat, t)

Le code complet de cette étape se trouve 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 

## Simulation

VITESSE_PERSONNAGE = 300
TAILLE_PERSONNAGE = 10
TAILLE_PROJECTILES = 10

## Rendu

COULEUR_ARRIERE_PLAN = (0, 0, 0)
COULEUR_PERSONNAGE = (255, 146, 205)
COULEUR_PROJECTILES = (146, 255, 205)

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

def initialiser_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

    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

class Projectile:

    def __init__(self, position):
        self.position = position

class Simulation:

    def __init__(self, personnage):
        self.personnage = personnage
        self.projectiles = [
            Projectile(Vector2(200, 300)),
            Projectile(Vector2(400, 120)),
            Projectile(Vector2(800, 600)),
            Projectile(Vector2(200, 600)),
        ]
        self.collision = False


def initialiser_simulation(etat):
    pos = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2)
    p = Personnage(pos)
    etat.simulation = Simulation(p)

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, TAILLE_PROJECTILES):
            etat.simulation.collision = True

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

def avancer_simulation(etat, t):
    deplacer_personnage(etat, t)
    detecter_collisions(etat)

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

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

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

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

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), TAILLE_PROJECTILES)

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

    rendre_projectiles(etat)

    rendre_personnage(etat)

    pygame.display.flip() 

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

def main():

    pygame.init()

    etat = GameState()

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

    initialiser_controles(etat)
    initialiser_simulation(etat)
    initialiser_rendu(etat, fenetre)

    clock = pygame.time.Clock()

    while not etat.controles.quitter:

        t = clock.tick(FPS)/1000

        traiter_controles(etat, t)

        if not etat.simulation.collision:
            avancer_simulation(etat, t)

        afficher_rendu(etat, t)

    pygame.quit()

if __name__ == "__main__":
    main()

En lançant le programme, puis en dirigeant le personnage vers un projectile, on peut voir la simulation se mettre en pause.