|
|
Page web Vincent THOMAS
Atelier ISN - 30/03/2017 "Moteurs physiques, jeux vidéos et comportements"
Plan de la page
Description de l'atelier
Actuellement, de nombreuses applications numériques utilisent des moteurs physiques que ce soit pour avoir des réactions réalistes dans des jeux (déplacements dans Super Mario, simulation de l'inertie du joueur dans les FPS, ...), pour proposer des simulateurs physiques (pédagogiques comme algodoo ou professionnels) ou pour aider le développement d'animations 3D (moteur dans Blender, ...).
Cet atelier cherchera à faire le lien entre la physique et l'informatique.
- (1) Dans un premier temps, nous écrirons ensemble un moteur physique très simple constitué de quelques instructions (Pyhton ou Java) et basé sur la mécanique du point (système masse-ressort).
- (2) Nous verrons ensuite comment l'étendre pour en faire un moteur de jeu vidéo.
- (3) Puis, nous tirerons parti des lois de la physique pour aborder le domaine de l'Intelligence artificielle et simuler des comportements de foule inspirés de la biologie (boids et flocking) et utilisés dans de nombreux domaines (Dimensionnement de bâtiments, Effets spéciaux, ...).
L'ensemble des fichiers est disponible dans cette archive zip
Diaporama et code des exemples
Les documents utilisés lors de la présentation de l'atelier
Parmi les choses non présentées dans l'atelier
Installation de pygame
Cet atelier utilisera le langage python et la librairie pygame destinée à faire des animations et des jeux. Cette libraire se révèle très pratique et facile d'accès et l'atelier permettra aussi de faire un point sur son fonctionnement.
Le plus simple semble de télécharger la dernière version Edupython une distribution python 3 destinée à l'enseignement et qui intègre pygame.
Pour les autres distributions, il est nécessaire d'installer pygame et d'avoir des libraires compatibles. Pour cela, vous pouvez récupérer et installer les fichiers suivants sous windows (python 3.2 avec pygame adapté)
Les pages suivantes proposent des liens permettant d'aborder facilement la librairie pygame
Un moteur simple avec pygame
1 - Moteur physique
La première étape consiste à écrire un moteur très simple simulant la mécanique du point.
On se limitera pour commencer à simuler le comportement d'un objet mobile soumis à la gravité et lancé avec une vitesse initiale.
Pour cela, il suffit de partir des équations aux dérivées partielles (acceleration = somme des forces) et de les intégrer en discrétisant le temps.
Ces équations se transforment simplement en affectation (avec un dt trés petit)
//definition de l'acceleration (gravite uniquement)
ax=0
ay=-9
//integration des equations de la dynamique v (temps discret)
vx=vx+ax.dt
vy=vy+ay.dt
x=x+vx.dt
y=y+vy.dt
Ecrire le code correspondant et afficher à l'écran les valeurs de position, vitesse et accélération. Pour structurer un peu mieux le code construit, il est possible d'encapsuler les lois physiques dans une classe contenant les données de l'objet mobile (par exemple jeu).
Les deux fichiers ci-dessous proposent un premier corrige:
2 - Présentation de la librairie pygame
Afin d'avoir un rendu graphique mis à jour au fur et à mesure du temps, nous allons utiliser un moteur de jeu. En python, nous nous focaliserons sur la librairie pygame.
La librairie pygame est une libraire destinée à développer rapidement et facilement des jeux vidéos. Elle propose toutes les primitives d'un moteur de jeu.
Un moteur de jeu est un ensemble de classe et de fonctions permettant de faire tourner un jeu indépendamment de son contenu (règles du jeu, ...). Un moteur de jeu consiste (1) à répéter de manière régulière une boucle de jeu constituée de la mise à jour du jeu et de son affichage (2) à prendre en compte les commandes faites par le joueur en entrée (évènement clavier, ...)
Une boucle de jeu peut simplement être représenté par l'algorithme suivant:
- (1) Initialisation du jeu
- (2) tant que le jeu n'est pas fini
- (2.1) prendre en compte les actions du joueur
- (2.2) mettre à jour les éléments du jeu
- (2.3) afficher l'état du jeu
- (2.4) attendre un certain temps
La librairie pygame propose trois éléments permettant de mettre en place très rapidement un moteur de jeu
- un moyen permettant de répéter de manière régulière la boucle de jeu (par exemple 50 fois par seconde). On parle de FPS (frame per second = nombre de fois où la boucle s'exécute en 1 seconde). Cela se fait par la classe Clock()
- un moyen de prendre en compte les évènements utilisateurs. Pygame stocke tous les évènements entre deux boucles de jeu dans un tableau d'évènements que l'on pourra interroger à loisir.
- un moteur graphique permettant de dessiner et d'afficher le rendu de l'état du jeu. Ce moteur utilise le principe du doublebuffering: pendant qu'une image est affichée à l'écran, on dessine sur une image cachée et lorsque le dessin est fini, on inverse les deux images.
3 - Moteur de jeu sous pygame
Le code suivant (inspiré de Programm arcade with pygame) présente un squelette simple permettant de développer un jeu sous pygame.
#on cree un jeu (approche objet)
jeu=Jeu()
# jeu_fini est un boolean qui précise quand le jeu est fini
jeu_fini = False
# on creer une horloge pour réguler la vitesse de la boucle de jeu
clock = pygame.time.Clock()
# -------- Boucle principale -----------
# tant que le jeu n'est pas fini
while not jeu_fini:
# --- on traite les evenements
jeu.traiter_evenement()
# --- on fait evoluer le jeu
jeu.evoluer()
# --- on dessine le jeu
jeu.dessiner()
# on inverse les affichages (double buffering)
pygame.display.flip()
# --- on demande d'attendre ce qu'il faut pour un FPS de 60
clock.tick(60)
# -------- Fin Boucle principale -----------
pygame.quit()
Dans ce code:
- la méthode traiter_evenement()
est en charge de traiter les évènements de l'utilisateur
- la méthode evoluer()
est en charge de faire évoluer le jeu
- la méthode dessiner()
est en charge dessiner l'état du jeu
Ce squelette permettant d'exécuter une classe de jeu est disponible dans le fichier suivant:
4 - Simulateur de chute
A partir de ces éléments, il est trés simple d'écrire un simulateur physique avec un rendu graphique.
Concernant la mise à jour des données, on se contentera d'utiliser les lois physiques discrétisées présentées ci-dessus:
//definition de l'acceleration (gravite uniquement)
ax=0
ay=-9
//integration des equations de la dynamique v (temps discret)
vx=vx+ax.dt
vy=vy+ay.dt
x=x+vx.dt
y=y+vy.dt
Pour l'affichage, on se limitera à dessiner un cercle avec la primitive de dessin pygame.draw.rect. Il faut néanmoins faire attention à changer le repère: les coordonnées (0,0) d'un écran correspondent au coin haut-gauche, il faut donc penser à inverser l'axe y pour passer du monde physique à l'écran.
RED = (0xFF, 0x00, 0x00)
pygame.draw.rect(screen,RED,(jeu.x,400-jeu.y,10,10))
Dans le code proposé ci-dessous, le changement de repère (monde-écran) est géré par la méthode changerCoordonnes ce qui permet d'ajouter une plus grande flexibilité. On peut ainsi facilement changer les coordonnées de références ou le facteur de zoom en ajoutant des paramètres supplémentaires (cf section ultérieure "changement coordonnées").
# permet de changer de repere pour l'affichage
def changerCoordonnes(self,dx,dy):
nx = dx
ny= 400-dy
return( (nx,ny))
Enfin, la seule interaction avec l'utilisateur résidera dans la possibilité de fermer la fenêtre lancée par pygame (on récupère l'évènement pygame.QUIT)
# --- on traite les evenements
for event in pygame.event.get():
#si l'utilisateur arrete
if event.type == pygame.QUIT:
done = True
5 - Affichage de la trajectoire
Il est possible d'afficher la trajectoire suivie en stockant les valeurs de (x,y) de l'objet à chaque itération.
Cela est fait dans la méthode evoluer() qui stocke les positions successives dans les attributs de type tableau trajX et trajY.
self.trajX+=[self.x]
self.trajY+=[self.y]
L'affichage consiste alors à tracer des lignes entre les points successifs de la trajectoire (à l'aide d'une simple boucle dans la methode dessiner()).
#on peut afficher la trajectoire stockee dans trajX et trajY
#pour chaque point de la trajectoire
for i in range(0,len(self.trajX)-1):
x=self.trajX[i]
y=self.trajY[i]
coord=self.changerCoordonnes(x,y)
x2=self.trajX[i+1]
y2=self.trajY[i+1]
coord2=self.changerCoordonnes(x2,y2)
#PS : on peut ameliorer la boucle car le x2 et y2 sont les futurs x,y
pygame.draw.line(screen,BLUE,coord,coord2,1)
6 - Controleur clavier
En récupérant les évènements claviers, il est possible d'interagir avec l'objet mobile.
A la réception d'un évènement correspondant à la touche "haut", il suffit de modifier la vitesse vy de l'objet mobile. Le moteur physique prend ensuite en charge son déplacement et l'intégration de la gravité.
La méthode controler() gére les évènements utilisateurs. Dans pygame, ceux-ci sont stockés dans un tableau (accessible via pygame.event.get()) qu'il suffit de parcourir à chaque iteration. On ajoute un paragraphe sur la gestion de l'appui d'une touche (le type de l'évènement correspond à la constante pygame.KEYDOWN)
# la gestion des touches
# appelee dans la boucle de jeu
def controler(self):
global done
#traitement des evenements
for event in pygame.event.get():
#si l'utilisateur arrete
if event.type == pygame.QUIT:
done = True
#gestion de l'appui d'une touche
if event.type == pygame.KEYDOWN:
# si c'est la touche "haut"
if event.key == pygame.K_UP:
print("up")
jeu.vy=50
7 - Angry birds
Une fois que le système de gestion de la gravité est mis en place, on peut imaginer de nombreux jeux basés sur ce type de moteur.
Par exemple, un jeu comme "angry birds" en est la conséquence directe:
- (1) gerer les evenements souris permet de determiner la vitesse initiale de l'objet tant que celui-ci ne vole pas
- (2) lorsqu'on lache le bouton de la souris, l'objet s'envole avec la vitesse initiale convenue et est soumis à la gravité
Ajouts au moteur
1 - Système masse-ressort
Une fois qu'on dispose d'un système permettant de simuler la mécanique du point, il est possible d'ajouter la notion de ressort.
Il suffit simplement de définir des ressorts entre des objets et de calculer à chaque itération les forces exercées entre les objets aux extrémités des ressorts. Le moteur physique s'occupe ensuite de prendre en compte ces accélérations dans la modification de la vitesse et de la position des objets.
Dés que la classe ressort est définie, il est possible de gérer plusieurs ressorts. Sur chaque particule mobile, on ajoute la contribution de chaque ressort à la somme des forces et on fait ensuite évoluer le système.
Il est aussi possible d'afficher à des positions régulières le vecteur force exercé par le ressort (car, dans ce cas, la force ne dépend que de la position de l'objet attaché au ressort). Il est ensuite possible de rendre compte des équilibres stables et instables en raisonnant sur le champ de force.
L'application ralentit beaucoup car à chaque itération, toutes les forces sont calculées et réaffichées (comme le champ de force ne bouge pas, l'idéal pour accélérer l'affichage consisterait à sauver ce champ de force dans une image qu'on recopiera à chaque itération)
2 - Systeme de ressort - Blob
Une fois le système masse-ressort écrit, il est possible de créer des jeux basés sur des créatures composées de ressorts.
Le plus simple consiste à créer une classe Systeme constituée
- d'une liste objets mobiles
- de la liste des ressorts liant ces objets mobiles
La méthode evoluer() de cette classe prend en compte tous les ressorts pour mettre à jour l'accélération des objets concernés puis intégrer le résultat sur chaque objet.
class Systeme():
# un systeme est constitue d'objets et de ressorts
def __init__(self):
self.ressorts=[]
self.objets=[]
# executer
def evoluer(self):
# initialiser a a g
for objet in self.objets:
objet.ax=0
objet.ay=-9
# pour chaque ressort
for ressort in self.ressorts:
ressort.metreAJourAcc()
# mettre à jour objet
for objet in self.objets:
objet.evoluer()
A partir de la classe Systeme, on peut produire des formes constituées de treillis de ressorts. Le comportement général de l'objet dépend de la manières dont les objets mobiles sont connectés.
Il est à noter que chaque objet du treillis dispose de sa propre masse (puisqu'on ajoute la gravité à l'accélération de chaque objet). Il est possible d'avoir des objets de masses différentes en modifiant le vecteur accélération (les forces autres que la gravité doivent être modulées par le rapport de masses m/m_unitaire).
La version ci-dessous ajoute un contrôleur qui permet de modifier en temps réel la raideur des ressorts (avec les flèches "haut" "bas" du clavier).
En fonction de cette raideur, le blob peut s'affaisser sous son propre poids.
En baissant la raideur puis en l'augmentant rapidement il est possible de faire "sauter" la forme (du fait de la reaction des ressorts)
jeu Gish
C'est sur cette base, que le jeu gish a été développé (mais il intègre beaucoup plus d'éléments en particulier, la manière de se déplacer en réponse aux contrôles des utilisateurs).
- Le code source de gish a été mis à disposition en 2010 par crypticsea, l'entreprise qui a développé le jeu.
- Une version basée sur le code de gish proposée par Ken Hoff, montre le rendu qu'il est possible d'obtenir avec des approches "soft body physics" (physique des corps mous)
3 - Gravité et système à plusieurs corps
De la même manière, simuler la gravité entre planètes consiste simplement à ajouter à notre moteur de jeu de nouveaux types de force comme la force de gravitations exercée par d'autres corps.
Cette force
- est dirigée selon la direction reliant les deux objets ; et
- a une intensité proportionnelle à la masse / distance au carré.
# definition des planetes sous la forme [x,y,rayon]
planete = [200,200,20]
# vecteur direction de la force
direction=[planete[0]-self.x,planete[1]-self.y]
# calcul distance entre balle et planete
dcarre=direction[0]*direction[0]+direction[1]*direction[1]
from math import sqrt
d=sqrt(dcarre)
# si la distance est plus petite que le rayon => crash
if (d < planete[2]):
self.enVol=False
return
# calcul de la force de gravite exercee en 1 / d^2
# (ici au cube car la direction n'est pas normalisée)
direction[0]=direction[0]/(d*d*d)
direction[1]=direction[1]/(d*d*d)
# ajoute contribution a l'acceleration
self.ax+=masse*direction[0]
self.ay+=masse*direction[1]
Il est ensuite possible d'ajouter plusieurs planètes.
Il est aussi possible d'ajouter planètes de "masse négative" pour générer des forces de répulsion comme le présente la figure ci-dessous (sur ce schémas, ce sont les planètes bleues qui repoussent la particule).
4 - Poussée d'Archimède
Il est aussi possible de représenter la poussée d'archimede sur une solide. Il suffit pour cela d'adapter l'acceleration verticale en fonction de la partie immergée de l'objet considéré.
# poussee totale
total=12
# calculer acceleration
# si en dehors de l'eau
if (self.y>0+self.taille/2):
self.ay = -9
partie = 0
else :
#totalement immerge
if (self.y<0-self.taille/2):
self.ay = -9+total
partie = 1
#partiellement immerge
else:
#calcul propotion immergee
partie = (self.taille/2-self.y)/self.taille
self.ay = -9+partie*total
#amortissement fonction de la proportion immergee
self.ay=self.ay-self.vy*0.2*partie
Jeu, collision et interaction
1 - Gestionnaire collision
Afin de faire un jeu, il faut rajouter un gestionnaire de collisions. Un gestionnaire de collision consiste avant tout à déterminer quand un objet rectangulaire intersecte un autre objet rectangulaire.
Pour gérer des objets de forme quelconque, on utilise ensuite des Bounding box à savoir des rectangles englobant l'objet à tester
if (b.x >= a.x + a.w)
or (b.x + b.w <= a.x)
or (b.y >= a.y + a.h)
or(b.y + b.h <= a.y)):
return false
else:
return true
}
Le code ci-dessus vérifie s'il y a collision entre deux rectangles a et b caractérisé par une position initiale (x,y) et une largeur et hauteur (w,h)
On sépare l'espace en quatre portions en fonction du rectangle A. Si le rectangle B se trouve dans une de ses portions, c'est qu'il n'y a pas collision.
2 - Rebond collision
Une fois la détection, faite, il est possible de gérer l'événement, par exemple en ajoutant un rebond (si la collision est en haut, ou en gauche/droite) et un appui si la collision est en bas avec le personnage.
Simulateur de comportements
Afin de simuler les comportements de déplacement, une approche désormais classique consiste à utiliser des steering behaviour (cf page de Craig Reynolds). Il s'agit de modéliser la dynamique de déplacement à l'aide de la mécanique du point.
La motivation de l'agent se traduit sous la forme de forces qui vont influencer sa vitesse et son déplacement.
1 - Accélération directe
La comportement le plus simple consiste à déterminer la direction de l'agent en fonction l'objectif à atteindre.
La commande en accélération consiste alors simplement à suivre la direction du vecteur (position agent -> objectif).
Cependant, ce type de comportement introduit des oscillations importantes puisque l'accélération change de direction uniquement quand la position de l'agent dépasse la position de l'objectif.
2 - Commande adaptée
Une première solution à contrôler par la vitesse. La commande en accélération a alors pour objectif de modifier la vitesse courante vers la vitesse souhaitée.
3 - Comportement de groupe
Il est alors possible de gérer des comportements de groupe en ajoutant des répulsion entre agents. Cela permet aux agents de ne pas se percuter tout en cherchant à attaindre l'objectif souhaité.
Le code suivant propose de faire plusieurs groupes. On controle l'objectif de chaque groupe avec un click gauche/droite. Tous les agents se repoussent les uns les autres ce qui permet de modéliser deux groupes qui se croisent.
4 - Ajout d'une caméra
En représentant explicitement la manière dont l'affichage calcule les coordonnées des points dans l'image, il est simple d'ajouter une caméra mobile permettant de changer de repère.
Les lois du monde restent les même et le code permettant de faire évoluer les grandeurs physiques ne change pas, seule la fonction permettant d'afficher le résultat à l'écran doit être modifiée.
Pour cela, le code suivant propose une classe Repere. Cette classe représente le lien entre le monde physique et l'écran. Elle contient 3 attributs importants: le centre de la caméra (x0,y0) et le facteur de zoom.
La classe Repere possède deux méthodes de transformation de coordonnées: une permettant de passer du monde physique au monde de l'image (pour afficher le système), une permettant de passer du monde de l'image au monde physique (pour pouvoir correctement repositionner un click de souris). La composition des deux fonctions doit retourner l'identité.
#passe du monde à l'ecran
def transformCoord(self,xm,ym):
tx=self.tailleFenetreX/2
ty=self.tailleFenetreY/2
return(tx+(xm-self.x0)*self.zoom,ty+(ym-self.y0)*self.zoom)
#passe de l'ecran au monde(ex gerer click souris)
def coord_inverse(self,xe,ye):
tx=self.tailleFenetreX/2
ty=self.tailleFenetreY/2
return((xe-tx)/self.zoom+self.x0,(ye-ty)/self.zoom+self.y0)
Le code correspond intègre ce changement de repère:
5 - Flocking
Le comportement de flocking est un comportement de déplacement en groupe en essaim (banc de poissons, essaim d'insectes ou nuée d'oiseaux). Ce type de comportement est caractérisé par l'absence de leader et le fait que chaque individu modifie sa trajectoire en fonction de ses voisins.
Ce comportement peut d'implémenter en utilisant le type de simulateur qui a été développé et s'articule autour de trois comportements élémentaires appliqués à chaque individu et basés sur leurs observations (cf référence Flocking behaviour):
- le comportement d'alignement : chaque individu a tendance à suivre la même direction que celle de ses voisins.
- le comportement de cohésion : chaque individu a tendance à se diriger vers le barycentre de ses voisins.
- le comportement de séparation : chaque individu a tendance à s'éloigner de chaque voisin.
L'application ci-dessous implémente un comportement de flocking. Il est possible de modifier l'importance de chaque comportement élémentaire en cliquant dans les zones de couleur.
Moteur de jeu en JAVA
Si vous souhaitez aborder le contenu de l'atelier avec JAVA, cette partie quelques éléments pour JAVA.
Tout d'abord, parmi les librairies utilisables, on peut noter slick2D qui permet de développer des jeux en JAVA
Cette section vous propose un moteur simple développé personnellement et proposé aux étudiants dans différentes formations
Les premiers exemples donnés dans le fichier zip implémentent les premiers éléments de l'atelier. Il suffit ensuite de ré-implémenter progressivement les choses qui sont présentées en python en suivant le déroulement de l'atelier.
Références utiles
- Sur les moteurs physiques
- Sur le moteurs de jeu
- Sur les steering behaviour
- Sur la modélisation de comportement
- Sur les problèmes de controle de dynamique
Icons designed by {Freepik} from Flaticon.
|
|
|