I. Récupérer les fichiers source▲
Un dépôt public GitHub est disponible, il contient trois projets représentant chacun une étape du développement du logiciel de travaux pratiques. Vous pouvez télécharger ces projets en vous rendant sur ce dépôt, cliquez sur code (bouton vert) et choisissez « download ZIP ».
Ensuite, chaque répertoire projet contient un .pro à ouvrir avec Qt Creator.
La documentation officielle Qt en anglais sur « The State Machine Framework » version 5.12 est consultable en ligne.
Vous trouverez les liens sur les différents tutoriels « Developpez » concernant Qt en fin de document.
II. Fondement▲
Au terme du cours précédent, sur les bases de la programmation avec le framework QStateMachine de Qt, nous avons assimilé les principes de l’utilisation de cette machine à état en nous familiarisant, avec la classe de base QState de cette bibliothèque, par la création d'états simples qui réagissaient à une ou plusieurs transitions. Durant cet apprentissage nous avons observé qu'au sein de cette machine un seul état était actif, et que si nous réunissions dans un état parent plusieurs états enfants, cet état parent et un seul de ses enfants pouvaient être actifs. Cette méthode de gestion des états résultait de l'utilisation, par défaut, du paramètre ChildMode la classe QState : un état parent qui regroupe plusieurs états enfants peut revêtir deux modes pour les gérer, soit le mode exclusif, qui est celui par défaut et que nous avons utilisé, soit le mode parallèle que nous allons étudier maintenant.
Les diagrammes affichés dans l’ouvrage figurent, au plus près possible, un lien entre l’analyse/programmation et les techniques utilisées par la bibliothèque Qt.
Dans le diagramme ci-dessous, deux machines sont représentées :
- machineA, qui englobe deux états exclusifs contenant chacun trois états exclusifs ;
- machineB, identique à machineA, sauf qu’un de ses deux états est déclaré parallèle.
Lorsque machineA est démarrée, Parent1 est activé et, conséquemment, Enfant11 aussi. Dans le cas de machineB, c’est Parent3 qui est initialisé au démarrage, mais sa déclaration parallèle fait que, à son déclenchement, ses trois états enfants sont activés en même temps. Enfin presque : ils sont démarrés dans l’ordre où ils ont été déclarés dans le fichier source. Nous remarquerons qu’il n’y a pas de symbole d’initialisation pour les états enfants inclus dans l’état parallèle, puisqu’il y a simultanéité d’activation.
Nous avons appris que, quand un état est activé, sa fonction onEntry est exécutée et, lorsqu’il est désactivé, c’est sa fonction onExit qui est appelée. Pour un état parallèle, son comportement personnel sera identique, mais s’il contient des états enfants, comme ils sont activés dans la foulée, leurs fonctions onEntry sont exécutées dans l’ordre de leur activation. Au moment de la désactivation, les fonctions onExit sont déclenchées dans l’ordre inverse de l’entrée.
Les états inclus dans un état parallèle peuvent être un mélange d’états exclusifs et d’états parallèles, qui contiennent eux-mêmes des états exclusifs et parallèles. Ce genre de mixage doit être parfaitement étudié, tant en entrée qu’en sortie, étant donné la difficulté pour détecter un dysfonctionnement dans cette structure.
Pour toucher du doigt l’intérêt de ce dispositif d’état parallèle, supposons que machine (à gauche dans le diagramme ci-dessous) est démarrée et que l’état enfant E du parent P est actif. Une transition déclarée entre E et E12 est mise en œuvre. De ce fait, E et son parent P sont onExit puis P1, E11 et E12 sont onEntry dans cet ordre, car, dans un état parallèle, quel que soit l’état enfant sollicité, l’ordre d’appel des fonctions onEntry suit la hiérarchie des déclarations dans le fichier source.
Avec cet exemple nous pouvons déduire trois évidences :
- au lieu de désigner comme cible un état enfant d’un état parent parallèle, il est plus judicieux de cibler l’état parent, puisque le résultat sera identique (voir la machine à droite dans le diagramme ci-dessus) ;
- si nous ajoutons un état enfant dans un état parent parallèle, toute transition ciblant cet état parent sera répercutée sur l’état enfant ajouté ;
- si, par inadvertance, une transition cible un état enfant d’un état parallèle, la suppression de l’état enfant annihile la transition, qui n’aura plus d’effet sur l’état parent.
Ces principes étant établis, nous les appliquerons à un cas concret en modifiant l’exemple du premier cours sur l’apprentissage des bases de la machine à états. Dans cet exemple, la machine gérait les états affectés à un objet pour piloter son animation grâce aux transitions déclenchées par l'appui de certaines touches du clavier.
Examinons un instant la classe QState, celle-ci contient une donnée de type ChildMode qui définit la propriété de ses enfants à être parallèles ou exclusifs :
enum
ChildMode {
ExclusiveStates, ParallelStates }
;
QState
(ChildMode childMode, QState
*
parent =
Q_NULLPTR
);
Jusque-là, nous avons ignoré cette donnée, puisque nous étions dans un environnement uniquement exclusif et que, lors de l’initialisation d’un objet QState, la valeur de childMode est ExclusiveStates par défaut. Nous utiliserons donc maintenant le constructeur adéquat tout en sachant que des méthodes permettent de gérer cette propriété.
...
ChildMode childMode() const
;
void
setChildMode(ChildMode mode);
...
Q_SIGNALS
:
...
void
childModeChanged(QPrivateSignal
) ;
III. États parallèles version simple — étape 1▲
Puisque la fenêtre graphique de ce logiciel de test est similaire au premier cours, nous ne l’étudierons pas et nous commencerons tout de suite par notre objet joueur qui circulera dans quatre directions.
Pour visualiser les effets de l’état parallèle, nous allons associer les directions à un état couleur et un état position. Ce dernier sera illustré par une rotation de l’objet. La règle sera que, si l’objet change de direction, il change de couleur et de position : comme ses états doivent s’activer simultanément, nous utiliserons un état parallèle parent.
Au départ du programme, le joueur est fixe, debout et incolore au centre de la scène, car il est placé dans l’état stop-P, état de démarrage dans machine. Cet état contient trois états enfants : arret, incolore et debout. Ils seront donc les propriétés de l’objet joueur chaque fois que cet état sera activé par l’appui sur la touche -5- ou l’interruption du déplacement de l’objet qui aura atteint une des limites de la zone graphique.
Un état conteneur réunit les états de déplacement du joueur de façon à appliquer le partage des transitions, comme nous l’avons étudié dans le premier cours, afin d’en limiter le nombre. Comme chaque déplacement suit le même schéma d’organisation, nous ne traiterons que le déplacement à gauche.
La touche de déplacement -6- stimule deux transitions :
- SversG : si l’état stop-P est activé et, de ce fait, l’objet est immobile ;
- versG : si l’objet est en cours de déplacement dans une autre direction que vers la gauche.
Dans ces deux cas, l’état parallèle Gauche-P sera activé, l’objet prendra la direction à gauche, aura la couleur rouge et subira une rotation de -90°.
Comme précisé au début de cette section, toute la partie environnement du programme, c’est-à-dire les classes constituant les objets fenêtres, boutons, séquence et vue, ne sera pas traitée. En cas de besoin, il faut se référer au premier cours. Nous n’étudierons que les classes nécessaires à la notion d’état parallèle.
III-A. Classe Joueur▲
Une première différence avec le premier cours est que l’objet joueur n’est pas paramétré dans le constructeur de cette classe, puisque chaque état parallèle produira son propre paramétrage en affichant son image pour la couleur, son angle pour la position et son sens pour le déplacement. En conséquence, au démarrage de la machine, suite à son initialisation à l’état stop_P, ce sont ses propriétés qui seront affichées.
Avec la création de l’état stop_P, nous remarquons l’utilisation du paramètre childMode pour lui affecter le mode parallèle. Ensuite, dans la création de ses états enfants, nous devons tenir compte de l’ordre de leurs déclarations, car lors de l’activation de l’état stop_P cet ordre sera suivi pour l’exécution des fonctions onEntry de chaque état enfant et, quand l’état sera désactivé, l’ordre inverse sera appliqué pour chaque fonction onExit.
QState
*
stop_P =
new
QState
( QState
::
ParallelStates, machine );
Arret *
arret =
new
Arret(this
, stop_P);
CouleurJoueur *
sanscouleur =
new
CouleurJoueur(this
, QString
( ":/motif/joueur.png"
), stop_P);
PositionJoueur *
debout =
new
PositionJoueur(this
, 0
, stop_P);
machine->
setInitialState(stop_P);
Nous créons l’état conteneur, chargé d’encapsuler les états du déplacement pour limiter les transitions. Nous lui ajoutons la transition qui se déclenche par l’activation du signal finished de l’animation témoignant du contact de l’objet joueur avec une limite de la zone graphique. L’état activé par cette transition serait obligatoirement arret dans un environnement exclusif, mais, dans notre cas d’état parallèle stop_P, nous pourrions cibler, stop_P, arret, sanscouleur ou debout. Le résultat serait toujours l’arrêt du joueur. Néanmoins, pour un suivi logique de notre programme, il est de fait que c’est l’état arret qui doit être ciblé et non la couleur ou la position.
QState
*
conteneur =
new
QState
(machine);
conteneur->
addTransition( animationdelobjet, SIGNAL
(finished()), arret );
QKeyEventTransition
*
stopjoueur =
new
QKeyEventTransition
( this
, QEvent
::
KeyPress,Qt
::
Key_5);
stopjoueur->
setTargetState(arret);
conteneur->
addTransition(stopjoueur);
À la suite, nous créons la transition stopjoueur déclenchée par l’appui sur la touche -5-, la remarque précédente en ce qui concerne sa cible arret est applicable ici aussi.
Pour la déclaration des quatre groupes d’états de déplacements, nous n’en examinerons qu’un seul, puisqu’ils sont de structure identique. Nous avons en premier l’état parallèle Gauche_P et dont le parent est conteneur. Cet état est directement issu de la classe QState, car nous n’avons pas de réimplémentation de ces fonctions à réaliser, il sert juste d’encadrement (dans cette étape).
QState
*
Gauche_P =
new
QState
( QState
::
ParallelStates, conteneur );
Bouge *
bougeagauche =
new
Bouge(this
, Gauche_P);
CouleurJoueur *
couleurrouge =
new
CouleurJoueur(this
, QString
(":/motif/jvr.png"
), Gauche_P);
PositionJoueur *
platgauche =
new
PositionJoueur(this
, 270
, Gauche_P);
Ensuite, nous déclarons les trois objets états bougeagauche, couleurrouge et platgauche, chacun issu d’une classe que nous étudierons par après. Ces trois objets ont comme parent l’état Gauche_P.
À ce niveau, nous avons deux transitions par état, déclenchées par l’appui sur une touche. Une transition est nécessaire pour démarrer l’objet et une autre pour le faire changer de direction en cours de déplacement.
TransDemarre *
SversG =
new
TransDemarre( this
, QEvent
::
KeyPress, Qt
::
Key₄ );
SversG->
setTargetState(bougeagauche);
arret->
addTransition(SversG);
De même que pour stopjoueur, la cible pourrait être soit l’état parallèle qui englobe, par exemple, Gauche_P, soit un de ces deux autres états enfants. De surcroît, ici, la transition pourrait être ajoutée à l’état parent stop-P ou n’importe lequel des ces états enfants. Toutefois, pour assurer un suivi aisé, il est préférable d’avoir une cohérence dans son analyse et la transition de démarrage ne peut être que la succession à l’arrêt et non au changement de couleur !
La transition de changement de direction est quasi une action interne entre l’état conteneur et la cible qui est un des quatre sens de déplacement.
TransChange *
versG =
new
TransChange( this
, QEvent
::
KeyPress, Qt
::
Key_4 );
versG->
setTargetState(bougeagauche);
conteneur->
addTransition(versG);
Les autres méthodes de la classe Joueur n’ont pas d’intérêt pour notre apprentissage. Vous pourrez les décortiquer, si besoin est, en examinant le fichier source.
III-B. Les classes état▲
Dans les descriptions qui suivent, j’ai déclaré une fonction onExit uniquement pour vous permettre l’utilisation des points d’arrêt du mode de débogage, de façon à observer l’ordre de désactivation des objets de ces classes si vous le souhaitez.
III-B-1. Classe Arret▲
La fonction onEntry de la classe Arret initialise le déplacement à 0 et appelle la fonction MajMouvement pour stopper le joueur.
Arret::
Arret( Joueur *
vjoueur, QState
*
vparent) : QState
(vparent)
{
joueur =
vjoueur;
}
void
Arret::
onEntry(QEvent
*
)
{
joueur->
FixerObjet();
joueur->
MajMouvement();
}
void
Arret::
onExit(QEvent
*
){}
III-B-2. Classe Bouge▲
Contrairement au premier cours, où l’important était d’observer le fonctionnement simple des états, ici, les états de déplacements sont instanciés sur la même classe Bouge. Elle ne contient qu’un appel à la mise à jour du déplacement.
Bouge::
Bouge( Joueur *
vjoueur, QState
*
vparent ) : QState
(vparent)
{
joueur =
vjoueur;
}
void
Bouge::
onEntry(QEvent
*
)
{
joueur->
MajMouvement();
}
void
Bouge::
onExit(QEvent
*
){}
Nous remarquons que la direction n’est pas définie dans cette classe, parce qu’elle dépend de la touche déclenchant la transition, c’est donc cette dernière qui la déterminera.
III-B-3. Classe CouleurJoueur▲
Dans cette classe, nous stockons l’image représentative de l’objet joueur, elle sera affichée par sa fonction onEntry :
CouleurJoueur::
CouleurJoueur(Joueur *
vjoueur,const
QString
&
vcoul, QState
*
vparent ) : QState
(vparent)
{
joueur =
vjoueur;
couleur =
QString
(vcoul);
}
void
CouleurJoueur::
onEntry(QEvent
*
)
{
joueur->
InitImage(couleur);
joueur->
setTransformOriginPoint( joueur->
boundingRect().center());
}
void
CouleurJoueur::
onExit(QEvent
*
){}
III-B-4. Classe PositionJoueur▲
La position est symbolisée par une simple rotation appliquée au QGraphicsObject dont hérite la classe Joueur.
PositionJoueur::
PositionJoueur( Joueur *
vjoueur, qreal
vrot, QState
*
vparent ) : QState
(vparent)
{
joueur =
vjoueur;
rotation =
vrot;
}
void
PositionJoueur::
onEntry(QEvent
*
)
{
joueur->
setRotation(rotation);
}
void
PositionJoueur::
onExit(QEvent
*
)
{}
Nous pouvons suivre en détaillant les fonctions onEntry de chacune de ces classes, le cheminement de l’activation des états pour obtenir l’effet sur l’écran graphique :
- Arret ou Bouge donne la mise à jour du déplacement ;
- CouleurJoueur modifie l’image de l’objet ;
- PositionJoueur change l’orientation de son trajet.
C’est la conséquence de l’utilisation de l’état parallèle qui englobe ces états exclusifs.
III-C. Les classes transition▲
Un des objectifs du dispositif de parallélisme est d’éviter la multiplication des transitions : pour activer les états enfants d’un état parallèle, il nous suffit d’une seule transition.
III-C-1. Transition TransDemarre▲
Dans cette transition, il n’y a pas besoin de tester la touche avec eventTest, puisque c’est son appui qui déclenche l’action. Il nous faut simplement implémenter sa fonction onTransition pour transmettre à l’objet joueur le code de la touche appuyée qui lui donnera la direction.
TransDemarre::
TransDemarre(Joueur *
vjoueur, QEvent
::
Type t, int
k) : QKeyEventTransition
(vjoueur, t, k)
{
joueur =
vjoueur;
touche =
k;
}
void
TransDemarre::
onTransition(QEvent
*
)
{
joueur->
InitDirection(touche);
}
III-C-2. Transition TransChange▲
Pour changer de direction, la touche appuyée doit être de direction différente du déplacement en cours, d’où l’implémentation de sa fonction eventTest pour vérifier si cette condition est réalisée.
TransChange::
TransChange(Joueur *
vjoueur, QEvent
::
Type t, int
k) : QKeyEventTransition
(vjoueur, t, k)
{
joueur =
vjoueur;
touche =
k;
}
bool
TransChange::
eventTest(QEvent
*
evt)
{
if
(!
QKeyEventTransition
::
eventTest(evt))
return
false
;
return
(touche !=
joueur->
CodeDirection());
}
void
TransChange::
onTransition(QEvent
*
)
{
joueur->
InitDirection(touche);
}
Si la condition est satisfaite, la fonction onTransition entre en œuvre et envoie le code de la touche à l’objet joueur.
Dans cette section, nous avons pris contact avec les états parallèles. Nous avons observé qu’il y a un ordre d’activation qui est inversé dans l’ordre de désactivation, et ce, quel que soit l’état enfant ciblé par la transition.
IV. Automatisation de l’inversion de déplacement — étape 2▲
Actuellement, quand le joueur est au contact avec une limite de la zone graphique, il s’arrête. Nous allons modifier la programmation pour inverser son déplacement dès le contact.
Avant toute chose, rappelons-nous que l’animation envoie le signal stateChanged quand l’objet se fixe. Néanmoins, il y a une différence entre l’arrêt de l’objet quand nous interrompons son trajet volontairement (par exemple, en appuyant sur la touche -5-) et l’arrêt de l’objet quand il arrive au bout de son trajet. Dans ce dernier cas l’animation émet le signal finished, à la suite du signal stateChanged.
Dans cette étape, nous en profiterons pour simplifier les états du déplacement dans le constructeur de la classe Joueur :
Dans le constructeur de la classe Joueur au lieu de :
QState
*
Droite_P =
new
QState
( QState
::
ParallelStates, conteneur );
Bouge *
bougeadroite =
new
Bouge(this
, Droite_P);
CouleurJoueur *
couleurbleue =
new
CouleurJoueur(this
, QString
(":/motif/jvb.png"
), Droite_P);
PositionJoueur *
PlatDroite =
new
PositionJoueur(this
, 90
, Droite_P);
Nous écrirons :
Bouge *
bougeadroite =
new
Bouge(this
, Qt
::
Key_6, conteneur);
bougeadroite->
setChildMode(QState
::
ParallelStates);
CouleurJoueur *
couleurbleue =
new
CouleurJoueur(this
, QString
(":/motif/jvb.png"
), bougeadroite);
PositionJoueur *
PlatDroite =
new
PositionJoueur(this
, 90
, bougeadroite);
À la création de l’objet bougeadroite, nous gardons le mode exclusif, que nous changeons aussitôt par un appel à setChildMode.
Nous remarquons l’envoi du code de la touche dans la classe Bouge pour inscrire la direction dans l’objet de l’état, car nous en avons besoin pour l’inversion de sens automatique au contact des limites de la zone graphique.
Puisque nous n’avons plus d’appel à l’état arret, au moment de ce contact, nous éliminons la ligne :
conteneur->
addTransition( animationdelobjet, SIGNAL
(finished()), arret );
Et en fin de constructeur nous ajoutons :
bougeagauche->
addTransition( animationdelobjet, SIGNAL
(finished()), bougeadroite);
bougeadroite->
addTransition( animationdelobjet, SIGNAL
(finished()), bougeagauche);
bougeenbas->
addTransition(animationdelobjet, SIGNAL
(finished()), bougeenhaut);
bougeenhaut->
addTransition(animationdelobjet, SIGNAL
(finished()), bougeenbas);
L’analyse de la première ligne nous montre que, si l’état bougeagauche est actif et que animationdelobjet arrive au terme de son trajet, la transition ciblera l’état bougeadroite pour l’activer.
Nous avons observé, plus haut, que la classe état Bouge avait subi une modification, elle devient :
Bouge::
Bouge(Joueur *
vjoueur, int
vdir, QState
*
vparent ) : QState
(vparent)
{
joueur =
vjoueur;
direction =
vdir;
}
void
Bouge::
onEntry(QEvent
*
)
{
joueur->
InitDirection(direction);
joueur->
MajMouvement();
}
void
Bouge::
onExit(QEvent
*
){}
La fonction onExit est présente pour placer un point d’arrêt pour suivre l’ordre d’activation et désactivation en mode de débogage.
Dans cette étape, nous avons fait une manipulation simple en changeant un état exclusif en état parallèle, ainsi que réalisé une transition d’un état exclusif inclus dans un état parallèle à un autre état exclusif inclus dans un autre état parallèle.
V. Inversion du mode d’un état – étape 3▲
La fonction setChildMode nous permet de changer le mode d’un état en cours d’exécution du programme. Pour démontrer cette possibilité, nous modifierons profondément l’étape 2, en ne conservant que le déplacement horizontal du joueur et en plaçant sur son chemin deux images figurant un trait bleu et un trait rouge. Lorsqu’il passera sur le trait bleu, l’état Gauche prendra le mode exclusif et lorsqu’il croisera le trait rouge ce même état redeviendra parallèle. Durant l’exclusivité de l’état gauche, le joueur reste bleu et garde sa position doigt vers la droite, un changement de direction ne modifiera que le sens de déplacement uniquement, pas la couleur ni la position. Quand l’état redevient parallèle par le contact avec le trait rouge, le premier événement changeant la direction remettra l’état gauche et ses enfants en coordination.
Pour analyser ce procédé, étudions le diagramme ci-dessous :
Si nous observons l’état à droite-P, nous lui attachons toutes les transactions. Nous aurions pu répartir ces transactions entre bleu et horizontal, puisque tous les états auraient été affectés par une transaction ciblant l’un d’entre eux, car ils sont enfants d’un état parallèle, mais la logique veut que la cible de ces transitions soit l’état du déplacement. Quant au mouvement à gauche, nous avons deux possibilités, soit un état conteneur parent, soit une structure identique à l’état à droite-P utilisant l’état à gauche comme parent. Ces deux raisonnements seront détaillés plus loin.
V-A. Création de la classe BorneAppel▲
Les transitions seront déclenchées par les bornes figurées sur la scène à l’aide de deux objets graphiques immobiles. La classe, qui est héritière de notre classe ObjetImage, sera sobre et le fichier entête ne comprend que la déclaration du constructeur qui initialise l’image :
class
BorneAppel : public
ObjetImage
{
public
:
BorneAppel( const
QString
&
vcoul);
}
;
typedef
QList
<
BorneAppel*>
LBorne;
À la suite de la déclaration de la classe, nous créons le type Lborne pour la liste des pointeurs sur les objets de la classe BorneAppel. Il est évident que nous pourrions créer uniquement deux instances d’objet, mais gardons la possibilité de créer une multitude de bornes si vous souhaitez réaliser un test plus exigeant ultérieurement.
Dans le constructeur nous plaçons l’image en arrière de la position « Z » de notre joueur en initialisant la valeur à « 2 », ainsi, le joueur dissimulera l’image de la borne lorsqu’il la croisera.
BorneAppel::
BorneAppel(const
QString
&
vcoul)
{
InitImage(QString
(vcoul));
setZValue(2
);
}
V-B. Modification de la classe Scene▲
Nous ajoutons dans l’entête de la classe Scene la déclaration de la liste des BorneAppel.
private
:
Joueur *
joueur;
Vue *
ptrvue;
LBorne lstborne;
Nous initialisons deux bornes, une bleue à gauche du joueur et une à droite.
BorneAppel *
tmpborne =
new
BorneAppel(QString
(":/motif/ajb.png"
));
tmpborne->
setPos(width*
0.25
,height*
0.5
);
tmpborne->
show();
addItem(tmpborne);
lstborne.append(tmpborne);
tmpborne =
new
BorneAppel(QString
(":/motif/ajr.png"
));
tmpborne->
setPos(width*
0.75
,height*
0.5
);
tmpborne->
show();
addItem(tmpborne);
lstborne.append(tmpborne);
joueur =
new
Joueur( &
lstborne);
La classe Joueur doit avoir connaissance du numéro d’entité de ces graphiques pour pouvoir les détecter durant son trajet, nous modifierons cette classe plus loin en lui créant un constructeur incluant l’accès au pointeur de la liste des bornes.
Et nous n’oublions pas de détruire les pointeurs de ces objets graphiques lors de la destruction de l’objet scene.
Scene::
~
Scene()
{
if
(joueur) delete
joueur;
if
(!
lstborne.empty())
{
for
(int
i=
0
;i<
lstborne.size();++
i) {
delete
lstborne.at(i); }
lstborne.clear();
}
}
V-C. Modification de la classe Joueur▲
Pour les modifications de cette classe, commençons par la déclaration des variables suivantes dans l’entête :
private
:
...
LBorne *
pborne;
QState
*
Gauche;
QState
*
Garage;
QState
*
Illusion;
La variable pborne stocke le pointeur de la liste des bornes lorsque le constructeur est appelé. L’état parent Gauche devant être disponible pour les autres fonctions de la classe hors de la méthode constructeur, nous déclarons son pointeur dans l’entête.
Les deux variables qui suivent de type QState* serviront uniquement pour mettre en œuvre la deuxième possibilité de modification de la structure d’état parallèle.
Le constructeur est doté d’un paramètre pour lui transmettre le pointeur de la liste des bornes à stocker.
Joueur::
Joueur( LBorne *
ptrb )
{
pborne =
ptrb;
duree =
180
;
....
La première possibilité pour inverser le mode du parent consiste à créer un état conteneur, réunissant trois enfants : bougeagauche, couleurrouge et platgauche :
Joueur::
Joueur( LBorne *
ptrb )
{
....
Gauche =
new
QState
(QState
::
ParallelStates,conteneur);
Bouge *
bougeagauche =
new
Bouge(this
, Qt
::
Key_4, Gauche);
CouleurJoueur *
couleurrouge =
new
CouleurJoueur(this
, QString
(":/motif/jvr.png"
), Gauche);
PositionJoueur *
platgauche =
new
PositionJoueur(this
, 270
, Gauche);
....
}
Dans cette possibilité, comme vu plus haut, nous ne devons PAS initialiser d’état de départ avec la fonction setInitialState dans l’état parallèle.
Dans le déroulement de l’action, notre objectif est de saisir l’instant où le joueur croise un des objets trait bleu ou trait rouge et pour enclencher cette opération, nous devons créer une connexion entre un signal, issu de l’animation, et une fonction slot qui sera la cible.
À la fin du constructeur nous créons l’instruction :
connect
(animationdelobjet,SIGNAL
(valueChanged(QVariant
)), this
, SLOT
(BorneContact()));
C’est dans cette fonction cible BorneContact que nous testons le contact, s’il est vrai et que la borne percutée est la bleue, l’état Gauche passe en mode exclusif sinon il passe en mode parallèle.
void
Joueur::
BorneContact()
{
if
(!
pborne->
isEmpty())
{
if
(BorneAppel *
pbornecontact=
TestCollision())
{
if
(pbornecontact==
pborne->
first())
Gauche->
setChildMode(QState
::
ExclusiveStates);
else
Gauche->
setChildMode(QState
::
ParallelStates);
}
}
}
La méthode TestCollision implémentée dans la classe Joueur utilise la fonction collidingItems de la classe QGraphicsItem et retourne la valeur du pointeur de la borne en cas de contact, sinon la valeur « nullptr » :
BorneAppel *
Joueur::
TestCollision()
{
QList
<
QGraphicsItem
*>
lstobjet =
collidingItems( Qt
::
IntersectsItemBoundingRect );
for
(int
i=
0
; i<
pborne→count(); i++
)
if
(lstobjet.indexOf(pborne→at(i)) !=
-
1
) return
pborne->
at(i);
return
nullptr
;
}
Lorsque ces changements seront effectués, une reconstruction du projet etape03 devrait vous permettre de constater les effets de l’inversion parallèle<>exclusif.
Avant de passer à la seconde possibilité, causons de setInitialState. Nous avons appris que dans un état parent en mode exclusif, nous avions l’obligation de déterminer un état Initial parmi les enfants, de façon à ce que la machine ait un point de départ quand l’état parent est activé. D’autre part, nous venons d’apprendre que dans un état parent en mode parallèle il ne faut pas d’état enfant initial. D’où cet antagonisme dans les deux modes d’état parent lorsque nous souhaitons passer de l’un à l’autre. Si nous créons un état parallèle et qu’il passe en mode exclusif, nous provoquerons une instabilité dans le comportement de la machine, car elle ne trouvera pas d’état initial, et inversement, si nous passons un état de mode exclusif qui contient un état enfant initial vers un mode parallèle, il y aura un risque d’interruption du programme.
En conséquence, pour examiner cette deuxième possibilité, nous modifions la structure de l’état parallèle en établissant l’objet bougeagauche comme parent et en le plaçant en mode parallèle avec l’état conteneur pour son propre parent. Les états couleurrouge et platgauche sont réaffiliés à bougeagauche, ainsi que l’état illusion servant de diversion chargée d’éviter les dysfonctionnements.
Bouge *
bougeagauche =
new
Bouge(this
, Qt
::
Key_4, conteneur);
bougeagauche->
setChildMode(QState
::
ParallelStates);
CouleurJoueur *
couleurrouge =
new
CouleurJoueur(this
, Qstring(":/motif/jvr.png"
), bougeagauche);
PositionJoueur *
platgauche =
new
PositionJoueur(this
, 270
, bougeagauche);
Illusion =
new
QState
(bougeagauche);
Gauche =
bougeagauche;
Garage =
conteneur;
Les deux variables Gauche et Garage vont nous permettre d’avoir accès aux pointeurs des objets dans la fonction BorneContact où les réassignations auront lieu.
if
(pbornecontact==
pborne->
first())
{
Gauche->
setChildMode(QState
::
ExclusiveStates);
Illusion->
setParent(Gauche);
Gauche->
setInitialState(Illusion);
}
else
{
Illusion->
setParent(Garage);
Gauche->
setChildMode(QState
::
ParallelStates);
}
Dans le cas du passage en mode exclusif : APRÈS avoir changé le mode de l’état Gauche, nous le réaffectons comme parent à l’état illusion que nous replaçons en état initial.
Dans le cas du passage en mode parallèle : AVANT de changer le mode de l’état Gauche, nous réassignons Garage (c’est à dire conteneur) comme parent à l’état illusion.
Nous venons de décrire que changer le parent d’un état initial lui fait perdre cette propriété, laquelle il nous faudra lui réaffecter au moment du retour en mode exclusif.
Quand ces changements seront réalisés, une compilation du projet etape03 nous permettra d’observer les mêmes effets que lors de l’expérimentation précédente.
Méditons sereinement sur l’étude de ces deux possibilités. Convenons que la première, avec son enveloppe « état parent parallèle » est la plus commode à utiliser. Mais elle nous souffle une question : pourquoi dans cette première possibilité nous n’avons pas besoin de toutes les précautions sécuritaires de la deuxième ?
Lors du deuxième cas l’objet de la classe état Bouge, bougeagauche, qui subit l’inversion de mode est la cible de plusieurs transitions, ainsi que le déclencheur de l’une d’elles :
...
SversG->
setTargetState(bougeagauche);
...
versG->
setTargetState(bougeagauche);
...
bougeadroite->
addTransition(animationdelobjet, SIGNAL
(finished()), bougeagauche);
bougeagauche->
addTransition(animationdelobjet, SIGNAL
(finished()), bougeadroite);
Ce qui provoque le message suivant qui définit bien l’incident :
Unrecoverable error detected in running state machine: Missing initial state in compound state ''
La machine a perdu ses petits !
D’où une règle simple de ne pas jouer au va-et-vient avec le paramètre ChildMode d’un état qui serait la cible de transition et/ou en serait le possesseur.
Pour vérifier cela, il suffit d’utiliser l’objet état platgauche comme état parallèle inversable :
PositionJoueur *
platgauche =
new
PositionJoueur(this
, 270
, conteneur);
platgauche->
setChildMode(QState
::
ParallelStates);
Bouge *
bougeagauche =
new
Bouge(this
, Qt
::
Key_4, platgauche);
CouleurJoueur *
couleurrouge =
new
CouleurJoueur(this
, QString
(":/motif/jvr.png"
), platgauche);
Gauche =
platgauche;
Et le déroulement du programme ne posera aucun souci, puisque l’état platgauche ne possède pas de transition, et n’est la cible d’aucune d’entre elles. Mais l’effet obtenu dans le déroulement du programme ne sera pas celui que nous souhaitions au début de ce cours.
Avec cette dernière étape, nous avons procédé à une manipulation complexe de la machine à états en accordant la possibilité à l’un de ses états de permuter de son mode d’exclusif vers un mode parallèle et vice-versa. Vous avez maintenant acquis la connaissance nécessaire pour réaliser, en situation confortable, des programmes utilisant le framework QStateMachine.
VI. Adaptations pour Qt 6.2▲
Avec Qt 6, les machines d'état ne sont plus disponibles dans le module Qt Core, mais bien dans un module à part, Qt State Machine. Pour utiliser le code source avec la version 6.2 de Qt, quelques petites modifications suffisent :
- dans les fichiers .pro, ajouter la ligne QT += statemachine ;
- dans les fichiers .h et .cpp où figure la ligne #include <QtCore/QStateMachine>, la modifier en supprimant QtCore : #include <QStateMachine> ;
- dans les fichiers .h où figure la ligne #include <QtWidgets/QKeyEventTransition>, la modifier en supprimant QtWidgets : #include <QKeyEventTransition> ;
- dans les fichiers .cpp où figure la ligne #include <QtCore/QSignalTransition>, la modifier en supprimant QtCore : #include <QSignalTransition>.
La compilation devrait se réaliser sans souci.
Il se peut que, lors del'installation de la version 6.2 de Qt, la bibliothèque Qt State Machine ne soit pas attachée, utilisez le logiciel MaintenanceTool de Qt pour la télécharger.
VII. Remerciements▲
Merci à dourouc05 pour ses conseils et à Claude Leloup pour la correction.