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
.
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
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.