Cours sur les états parallèles avec le framework QStateMachine

Daniel Génon

Après avoir étudié les bases des machines à états avec la bibliothèque Qt, vous pouvez enrichir ces connaissances en découvrant le principe des états parallèles et les utiliser dans cet environnement.

Commentez

- Titre : Cours sur les états parallèles avec le framework QStateMachine

- Auteur : Daniel Génon

- Parution : 27 juin 2021

- Mise à jour : 27 novembre 2021

- Public visé :

- Licence : Le contenu de cet article est rédigé par Daniel Génon et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Voir également :

- Utiliser le framework QStateMachine

- Documentation QStateMachine

- Tutoriels Qt

Sommaire

- I. Récupérer les fichiers source

- II. Fondement

- III. États parallèles version simple — étape 1

- III-A. Classe Joueur

- III-B. Les classes état

- III-B-1. Classe Arret

- III-B-2. Classe Bouge

- III-B-3. Classe CouleurJoueur

- III-B-4. Classe PositionJoueur

- III-C. Les classes transition

- III-C-1. Transition TransDemarre

- III-C-2. Transition TransChange

- IV. Automatisation de l’inversion de déplacement — étape 2

- V. Inversion du mode d’un état – étape 3

- V-A. Création de la classe BorneAppel

- V-B. Modification de la classe Scene

- V-C. Modification de la classe Joueur

- VI. Adaptations pour Qt 6.2

- VII. Remerciements

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.