INFO702 - TPs
|
L'objectif de ce TP est de vous familiariser avec la programmation objet classique en C++. L'idée est de partir d'une base de code déjà conséquente et de l'enrichir progressivement. Le focus est sur le polymorphisme (donc les méthodes virtuelles). Le prétexte est de vous faire travailler sur un programme graphique temps réel, qui gère plein d'objets différents dont on devra détecter les collisions.
La généricité se fera donc au travers du polymorphisme. A l'issue de cette séance, vous maîtriserez:
Les sites suivants pourront être utile pendant le TP:
make
démarre le TP en 5 minutes.ios::binary
ou ios::text
à l'ouverture des fichiers)On va donne une archive avec quelques fichiers tout prêts:
Pour compiler un projet Qt, on utilise qmake
, qui va créer un fichier Makefile
, puis make
. Si vous avez installé Qt dans un répertoire exotique, il faut donne le chemin complet de qmake
. Ensuite tout est bien configuré.
$ qmake $ make $ ./collider # sous Linux $ ./collider.app/Contents/MacOS/collider # sous MacOS
Normalement, vous avez un programme qui vous affiche quelque chose du genre ci-dessous:
Prenez le temps de regarder le code fourni (en commençant par collider.cpp, puis objects.hpp and objects.cpp) et éventuellement chercher quelques explications dans la doc Qt [https://doc.qt.io/qt-5 doc Qt5] [https://doc.qt.io/qt-6 doc Qt6].
On notera la hiérarchie suivante pour nos formes graphiques (cf. class GraphicalShape):
Evidemment, Qt offre déjà pas mal de choses dans la classe QGraphicsItem, mais nous allons faire nos propres formes graphiques avec algorithme randomisé de détection de collisions.
Vous allez définir deux classes. D'abord une classe Rectangle
, qui, sur le modèle de Disk, hérite de GraphicalShape. Elle représente un rectangle quelconque. Pour afficher un rectangle avec Qt, on utilise la méthode QPainter::drawRect. N'oubliez pas de redéfinir toutes les méthodes abstraites (i.e. virtuelles pures) de GraphicalShape.
Vous ferez ensuite une classe SpaceTruck
, qui hérite de MasterShape, et dont la représentation graphique est un rectangle vert, qui devient jaune lors d'une collision. Au niveau des mouvements, il se déplacera en avant et en tournant à droite en permanence. On utilisera opportunément les méthodes héritées de QGraphicsItem, QGraphicsItem::rotation
et QGraphicsItem::setRotation
, pour faire ce mouvement dans SpaceTruck::advance
.
Voilà un résultat possible avec 10 astéroides, 5 space trucks de taille 100x20.
this->setGraphicalShape
sur cet objet graphique.Asteroid::advance
. Le principe pour avancer tout droit est de faire un point (speed,0) (donc un peu à l'"avant" de la forme qui est centré en (0,0)), et de demander les coordonnées correspondantes dans le repère du parent. Comme on change la position via setPos
, ça déplace l'astéroïde.On va fabriquer des formes plus complexes par assemblage de formes simples. Pour ce faire on va créer une forme graphique Union
, qui hérite de GraphicalShape, et dont le seul rôle est de grouper deux GraphicalShape. On note les points suivants:
Union::randomPoint
: il faudra tirer une fois sur deux un point dans la forme 1 et un point dans la forme 2. Le plus simple est de tirer un entier aléatoire et d'appeler l'un ou l'autre en fonction de la parité. Une solution plus évoluée est de calculer les aires A1 et A2 des boites englobantes des deux formes, et de demander un point dans la forme 1, si rand01() < A1/(A1+A2
, sinon de demander le point dans la forme 2.Union::isInside
il faudra bien tester si le point appartient à une des formes.operator|
sur les QRect pour calculer le rectangle englobant de deux rectangles.dans Union::Union
il faut préciser à Qt la hiérarchie d'objets graphiques pour qu'il s'occupe de leur affichage. On utiliser setParentItem
ainsi:
Union::paint(...)
est alors définie, mais ne contient pas de code, car Qt (grâce à setParentItem
) peut aller chercher tout seul les autres objets graphiques.On pourra ensuite mettre à jour SpaceTruck
pour qu'il soit composé de formes simples différentes.
Pour composer des formes complexes, il nous reste à nous donner des transformations géométriques pour pouvoir tourner et placer nos formes. On va définir une classe Transformation
, qui prendra en paramètre un GraphicalShape f, un vecteur de déplacement dx et un angle de rotation angle:
setParentItem( _f )
puis utiliser this->setPos
et this->setRotation
pour placer Transformation
correctement dans son repère. On mémorisera aussi en donnée membre, à la fois le déplacement dx
et l'angle angle
, voire même on peut précalculer cos( angle * pi/180 )
et sin( angle * pi/180 )
.On veillera ensuite à redéfinir les méthodes usuelles:
Transformation::randomPoint
: on tire le point aléatoire dans la forme f, puis on applique la rotation et enfin la translation.Transformation::isInside( p )
: on va dans l'autre sens, on soustrait le déplacement, on applique la rotation inverse et on demande à f si le point obtenu est dedans.Transformation::boundingRect
il suffit d'utiliser la méthode héritée mapRectToParent
qui fait tout le calcul voulu de rotation d'une boite !Transformation::paint(...)
le code est vide, car Qt (grâce à setParentItem
) peut aller chercher tout seul les autres objets graphiques.\( x' = x * \cos(a) - y * \sin(a), \qquad y' = x * \sin(a) + y * \cos(a) \).
On pourra alors s'amuser à faire des objets plus complexes en composant nos formes. On pourra ainsi créer un vaisseau spatial sous la forme d'une classe Enterprise
qui hérite de MasterShape. On choisira une couleur grise pour le vaisseau et rouge en cas de collision.
Le code suivant vous place les éléments graphiques dans le constructeur de Enterprise
:
On pourra ensuite s'amuser à faire des déplacements un peu plus aléatoires pour le vaisseau. Vous devriez avoir maintenant une application qui ressemble à ça:
En fait, on peut étendre le principe précédent à des images quelconques. L'idée est la suivante:
QPixmap
de l'image, on fabrique un QBitmap
qui est son masque plein/vide.isInside
) lorsqu'il tombe dans un pixel plein du masque.Pour charger une image, il suffit de déclarer un QPixmap
et de lui donner un nom de fichier listé dans les ressources (cf collider.qrc
). On utilisera le format GIF qui peut stocker de la transparence.
On écrira donc ensuite une classe ImageShape
qui hérite de GraphicalShape. On la construit en lui donnant un QPixmap
et un MasterShape. On suppose qu'elle occupe les coordonnées (0,0) -> (w-1,h-1) où w et h sont la largeur et la hauteur de l'image.
Le QBitmap
_mask
servira lors des affichages de collision. C'est une structure optimisée pour l'affichage. Le QImage
_mask_img est quant à lui optimisé pour les requêtes pixel par pixel et servira pour ImageShape::randomPoint
et ImageShape::isInside
.
Ecrivez ces méthodes selon le principe explicité ci-dessus. On utilisera la méthode QImage::pixelIndex
qui retourne 0 sur du vide et 1 sur du plein.
_mask_img.pixel( p )
fonctionne si le QPoint p
est à l'intérieur de l'image. Dans la méthode ImageShape::isInside
, on vérifie donc d'abord si le point est dans la boite englobante de l'image ImageShape::boundingRect
avant de tester si ce point touche un pixel du masque de l'image.Pour l'affichage, on peut utiliser le code suivant:
Il ne reste plus qu'à faire une classe NiceAsteroid
qui s'occupe de créer un joli astéroïde tournant sur lui-même. Il aura besoin de deux transformations t1
et t2
. La première s'occupe de ramener l'image au centre (car celle-ci est entre les coordonnées (0,0) et (largeur,hauteur). La deuxième s'occupe juste de la rotation.
Dans NiceAsteroid::advance
on augmentera régulièrement l'angle de la transformation t2
, stockée dans la donnée membre _t
ci-dessous:
Transformation::setAngle
et Transformation::angle
pour lire/changer l'angle d'une transformation.Vous devez avoir maintenant quelque chose qui ressemble à ci-dessous, en ayant remplacé les Asteroid par des NiceAsteroid
.
Vous avez toute une base pour développer un petit jeu ou des petites animations. On peut citer les développements suivants:
N'hésitez pas à me poser des questions si vous ne voyez pas comment démarrer.
TP-XXX-YYY.*
où XXX
et YYY
désignent les noms respectifs des étudiants du binôme.