De retour à la construction d’environnement. Pour finir cette boucle sur Box2D, il est important de parler encore des forces et des collisions entre objets. En effet un des objectifs de NOC est d’être capable d’implémenter un environnement vivant en osmose et évoluant sous les yeux de l’utilisateur souvent indépendamment de son action.
- Forces
https://github.com/Thibautomaton/NOCchapter5Forces/tree/main
Dans les forces, on a vu que la gravité est souvent implémentée dès l’initialisation du monde Box2D, avec la fonction :
world = Box2D.b2World(gravity = (0, -10), doSleep=False)
On a donc des objets qui subissent déjà une force, celle de l’attraction vers la Terre. Mais qu’en serait-il si on voulait simuler des astres ? Ce sont des objets qui ont une force d’attraction les uns avec les autres et qui déroulent sur des mouvements (collisions, trajectoire elliptique, …).
Voyons comment créer un objet attracteur qui exerce une force sur les autres objets de l’environnement.
On commence par créer un monde Box2D avec une gravité mise à zéro. Dans la modélisations d’astres, la gravité n’existe pas car c’est une force locale liée à l’attraction de la terre, elle prend une autre forme à l’échelle macroscopique sous la forme d’une autre équation : g *masseA *massB / (distanceAB^2)
world = Box2D.b2World(gravity=(0, 0), doSleep=False)
On crée un objet Attractor et une liste d’objets encore vide:
attractor = Attractor(world)
movers = []
On voit que dans la boucle while, on calcule les force d’abord avec la fonction attract de Attractor, puis on l’applique à l’objet Mover via la fonction applyForce. Voyons la définition de ces deux fonctions :
Attractor est un objet Box2d avec un body une shape, il possède aussi une position fixe, un rayon r, une masse, un coefficient G.
def attract(self, mover):
force = self.body.worldCenter – mover.body.worldCenter
distance = force.length
distance = min(25, max(5, distance))
force.Normalize()
strength = (self.G*self.mass * mover.mass)/(distance**2)
force = force* strength
return force
Dans attract on calcule d’abord le vecteur entre les deux objets, on garde la distance de ce vecteur et sa direction en le normalisant. On calcule la valeur de la force via la formule de l’attraction, et on multiplie cette valeur et le vecteur de direction avant de retourner la valeur de la force.
Mover est un objet comme Attractor avec un body et une shape, sauf qu’il est mobile et a une masse variable.
La fonction applyForce est finalement assez simple puisque l’intention est comprise par Box2D.
def applyForce(self, force):
pos = self.body.worldCenter
self.body.ApplyForce(force, pos, wake=True)
On a quand même besoin de préciser les coordonnées d’application de cette force, ici le centre de masse situé au milieu de l’objet.
- Collisions
https://github.com/Thibautomaton/CollisionsNOCChapter5/tree/main
Il peut être à propos de savoir que l’on peut détecter lorsqu’un collision a lieu dans Box2D, afin d’exécuter un programme à ce moment-là. Comme si on implementait le jeu angry birds, on voudrait interagir quand notre oiseau rencontre un cochon ou un mur.
Dans cet exemple nous allons créer des objets cercles de Box2D qui change de couleur lors de leur interaction avec le sol, et sont supprimés lorsqu’ils collident entre eux.
Tout d’abord nous créons un monde o`u il y a la gravité :
world = Box2D.b2World(gravity = (0, -10), doSleep=False)
Nous allons faire appel à une interface. On crée donc un objet ContactListener associé à world qui va activer la fonction d’attendre une collision:
world.contactListener = MyContactListener()
Voyons la définition de notre contact listener :
class MyContactListener(b2ContactListener):
def BeginContact(self, contact):
a = contact.fixtureA.body.userData
b = contact.fixtureB.body.userData
if isinstance(a, Particle) and isinstance(b, Particle):
if a.lifespan<950:
a.markedFD =True
print(« delete »)
if b.lifespan<950:
b.markedFD = True
print(« delete »)
def EndContact(self, contact):
a = contact.fixtureA.body.userData
b = contact.fixtureB.body.userData
if isinstance(b, Bordure) and isinstance(a, Particle):
a.border = (0, 0, 255)
elif isinstance(a, Bordure) and isinstance(b, Particle):
b.border = (0, 255, 0)
On a deux fonctions, d’abord BeginContact qui intervient au début de la collision et EndContact à la fin. Il peut y avoir d’autres méthodes mais ce sont sur celles-là que s’attarde le livre NOC.
On voit que l’on récupère les informations de chaque objet via contact.fixtureObjet.body.userData. On peut ensuite identifier ces objets avec isinstance().
On commence avec un lifespan de 1000 pour chaque particules, on attend donc 50 frames avant de détruire l’objet pour ne pas les détruire dès leur génération.
Mettre la variable markerFD à True est une façon de détruire l’objet via la méthode isDead() de Particle.
Dans la méthode EndContact, donc à la fin de l’interaction on va tester si l’un des objets est une bordure dans quel cas on change la couleur de l’objet Particle.
CONCLUSION
Nous voici beaucoup mieux équipés pour interagir avec les mondes Box2D créés. Il est possible de produire tout type de visualisation de cette façon. Nous espérons que ces quelques lignes vous ont éclairé sur l’utilisation de Box2D et que vous n’hésiterez pas à utiliser ces informations dans votre programmation.
