Cours sur le framework QStateMachine

Daniel Génon

L’utilisation de ce framework de Qt peut paraître compliquée… Et elle l’est ! Mais elle permet d’obtenir une maintenance commode et sûre dans la progression d’un logiciel.

Pour assimiler le contenu de ce livre, vous devez maîtriser un minimum du langage C++ et des bibliothèques Qt.

Dans le discours qui suit, je décrirai un exemple de logiciel rudimentaire par phase de développement en améliorant son utilisation. J’ai créé ce logiciel constitué d’une animation graphique très simple et m’en suis inspiré pour élaborer cette documentation afin de comprendre les principes de la gestion des états.

Commentez

- Titre : Cours sur le framework QStateMachine

- Auteur : Daniel Génon

- Parution : 8 octobre 2020

- 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 :

- tutoriels Qt

- documentation QStateMachine

- animation avec Qt

Sommaire

- I. Récupérer les fichiers source

- II. Petit rappel

- II-A. Destination

- II-B. Les classes usuelles

- II-B-1. QStateMachine

- II-B-2. QState

- II-B-3. QFinalState

- II-B-4. QSignalTransition

- II-B-5. QEventTransition

- II-B-6. QKeyEventTransition

- II-B-7. QMouseEventTransition

- III. Fenêtre principale : étape fenetre

- III-A. La classe Fenetre

- III-A-1. Description du fichier source fenetre.cpp

- III-B. La classe ZoneAnime

- III-C. La classe Sequence

- III-D. La classe Scene

- III-D-1. Description du fichier source scene.cpp

- IV. Déplacement dans une direction - étape 01

- IV-A. Classe ObjetImage

- IV-B. Classe ObjetMobile

- IV-C. Classe Joueur

- IV-D. État BougeADroite

- IV-E. État Arret

- IV-F. Modification de la classe Scene

- IV-G. L’animation

- V. Déplacement dans deux directions contraires

- V-A. Version de base

- V-A-1. Modification des classes existantes

- V-A-1-a. Classe ObjetMobile

- V-A-1-b. Classe Joueur

- V-A-2. Nouvelles classes

- V-A-2-a. Classe BougeAGauche

- V-A-2-b. Classe TransDemarre

- V-A-2-c. Classe TransInverse

- V-B. Version améliorée - étape 02

- V-B-1. Partage de transitions

- V-C. Version finale - étape 03

- V-C-1. Modification des classes existantes

- V-C-1-a. Classe ObjetMobile

- V-C-1-b. Classe Joueur

- V-C-1-b-i. Importance de l’ordre de l’ajout des transitions

- V-C-1-c. TransInverse}

- V-C-2. Nouvelles classes

- V-C-2-a. TransDemarre

- V-C-2-b. TransAccelere

- V-C-2-c. TransFrein

- VI. Déplacement dans les quatre directions - étape 04

- VI-A. Nouvelles classes

- VI-B. Modification des classes existantes

- VI-B-1. TransAccelere

- VI-B-2. TransInverse

- VI-B-3. TransFrein

- VI-B-4. TransDemarre

- VI-B-5. ObjetMobile

- VI-B-6. Joueur

- VII. Objet non contrôlé

- VII-A. Déplacement d’un objet libre - étape 05

- VII-A-1. La classe Predateur

- VII-A-2. L’état DeplacePrd

- VII-A-3. L’état ArretPrd

- VII-A-4. Modification de la classe Scene

- VII-B. Série d’objets libres - étape 06

- VII-B-1. Modification de la classe Predateur

- VII-B-2. Modification de la classe Scene

- VIII. Objet contrôlé et objet libre - étape 07

- VIII-A. La classe Scene

- IX. L’objet contrôlé est une cible – étape 08

- IX-A. Modification de la classe Scene

- IX-B. Modification de la classe ObjetMobile

- IX-C. Modification de la classe Predateur

- IX-D. État ArretPrd

- IX-E. État DeplacePrd

- IX-F. Classe TrContact

- X. Un objet produit un autre objet

- X-A. Démarrer un objet - étape 09

- X-A-1. Modification de la classe Joueur

- X-A-2. La transition TransEtPan

- X-A-3. Modification de la classe Scene

- X-A-4. La classe Projectile

- X-A-5. État ArretPjl

- X-A-6. État DeplacePjl

- X-B. Déclencher une série d’objets – étape 10

- X-B-1. Modification de la classe Joueur

- X-B-2. Modification de la classe Scene

- X-B-3. Modification de la classe Projectile

- XI. Adaptations pour Qt 6.2

- XII. Remerciements

I - Récupérer les fichiers source

Un dépôt public GitHub est disponible, il contient dix 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 ici.

Vous trouverez les liens sur les différents tutoriels de Developpez.com concernant Qt en fin de document.

II - Petit rappel

II-A - Destination

Un objet issu d'une classe Qt contient les données et fonctions nécessaires à son utilisation ; par ailleurs, nous avons la possibilité de faire interagir des objets entre eux avec les mécanismes typiques à Qt : « signals » et « slots », ce qui les rend communicants. Dans ce qui suit, nous apprendrons à rendre un objet autonome en lui conférant la capacité d'adopter un état ou un comportement entre plusieurs que nous lui aurons attribués, et nous nous permettrons, par des transitions, de changer son état en cours. Ainsi nous acquerrons le moyen de métamorphoser un objet en un genre d'automate. En mathématiques, un automate peut-être défini comme un spécimen de modélisation. Brièvement, l'automate fini, qui est le plus élémentaire de cet univers mathématique, est utilisé dans la conception de logiciel informatique pour décomposer l'étude des fragments complexes du projet au plus simple possible. Ce modèle d'automate revêt un seul état parmi ceux qui lui sont assignés. Comme exemple je citerai :

- un tiroir possède deux états : ouvert ou fermé, et il ne peut être que dans un seul de ces deux états :

- en considérant que le tiroir démonté est un état possible, alors nous obtenons trois états, et le tiroir ne peut être que dans l'un de ceux-ci :

- nous pouvons pousser plus loin puisque l'état démonté ne peut être qu'une suite de l'état ouvert. Nous l'insérons donc à l'intérieur :

Dans le décorticage des états possibles dans un projet, nous nous exposons à en produire une surabondance. En reprenant l'exemple du tiroir : dans son état fermé, nous pourrions y inclure l'état fermé à clé… D'où la nécessité dans un développement informatique basé sur des états, d'avoir la possibilité de le modifier confortablement.

Pour représenter graphiquement un concept basé sur un automate fini, il y a plusieurs méthodes, dont les diagrammes du langage UML (Unified Modeling Language), et c'est ce modèle qui a inspiré Qt pour créer l'interface de programmation QStateMachine. Notez que dans la version Qt 6.0, QStateMachine a été supprimé temporairement de Qt Core et reviendra dans les versions ultérieures de Qt (probablement dans le cadre du module Qt SCXML) : voir ici.

La structure de machine à états est un ensemble de classes permettant de créer des états pour un ou plusieurs objets, et de définir des transitions pour que ce ou ces objets passent d'un état à un autre. Le démarrage de la machine à états affectée à l'objet gère ces transitions qui sont déclenchées par l'objet lui-même, ou par un autre objet, ou par une action externe. En général, un objet qui change d'état déclenche l'exécution d'une fonction et/ou un changement d'une ou plusieurs de ses propriétés. Pour cerner plus simplement l'usage de QStateMachine, tentons une analogie avec le principe des boucles sans fin dans un programme. Considérons qu'une machine est une de ces boucles. Elle a donc trois états : soit démarrée, soit suspendue, soit arrêtée. Au cours du déroulement d'un logiciel contenant des objets dotés de ce type de machine, si nous démarrons les machines de ces objets, elles analyseront en permanence les transitions qui leur sont assignées, et ce, quelle que soit l'activité du logiciel. Ce qui peut sembler rendre ces objets indépendants, dans le contexte de ce logiciel. Dans le cadre de cet apprentissage, notre objectif sera simple : transformer un objet informatique en automate en lui imputant des états dont les transitions, d'un état à un autre, provoqueront un comportement différent, ou influeront sur l'état d'un autre objet.

Dans le diagramme ci-dessous, l'état arret possède une transition deplace qui, si activée, déclenche l'état bouge ; celui-ci possède une transition stoppe qui, si activée, revient à l'état arret :

Dans ce cas de figure, chaque transition déclenche la fonction onEntry présente dans chaque état. Comme exemple pour un déplacement de l'objet : la transition deplace activera la fonction onEntry de l'état bouge qui démarrera une animation où l'objet se déplacera tant que la transition stoppe n'est pas déclenchée.

Toute entrée dans un état déclenche sa fonction onEntry, et toute sortie déclenche sa fonction onExit.

II-B - Les classes usuelles

Nous effleurons dans cette section les classes indispensables à l’utilisation de QStateMachine et quelques classes simples. Les textes qui suivent sont des traductions adaptées de la documentation originelle.

II-B-1 - QStateMachine

Une machine à états gère un ensemble d'états et de transitions connectées entre ces états qui définissent une arborescence d'état. Lorsque cette arborescence est réalisée, elle est exécutable par la machine d'état. Un état initial doit être défini avant de pouvoir démarrer la machine : c'est son état au démarrage. Vous pouvez ensuite lancer la machine qui sera en attente des événements.

II-B-2 - QState

La classe QState fournit les objets état traités par la machine. Elle est aussi utilisée comme base pour créer de nouvelles classes. Ces classes peuvent avoir des états enfants et peuvent avoir des transitions vers d'autres états.

Les deux fonctions virtuelles principalement implémentées de cette classe sont :

virtual void QAbstractState::onEntry(QEvent *evt);

cette fonction est déclenchée lorsque l'objet entre dans un état. L'événement passé en paramètre est le déclencheur de l'entrée ;

virtual void QAbstractState::onExit(QEvent *evt);

cette fonction est déclenchée lorsque l'objet quitte l'état. L'événement passé en paramètre est le déclencheur de la sortie.

II-B-3 - QFinalState

Nous pouvons assimiler une machine à états à une boucle sans fin, elle ne se terminerait jamais ; elle doit donc inclure un état final au niveau supérieur qui sera appelé pour l'arrêter.

QFinalState *final = new QFinalState(machine);

connect(machine, SIGNAL(finished()), qApp, SLOT(quit()) );

Pour ce faire, il suffit d'inclure un objet de la classe QFinalState et de le connecter par une transition dans l'arborescence des états.

Dans ce diagramme, l'instance objet final de la classe QFinalState, est en dehors de l'état zoneanime, donc hiérarchiquement au niveau supérieur :

II-B-4 - QSignalTransition

Cette classe crée des transitions déclenchées par la directive Signal des classes Qt.

Les deux fonctions virtuelles principales qui suivent peuvent être réimplémentées :

virtual bool eventTest(QEvent *evt); — cette fonction autorise un test qui déclenchera, ou pas, la transition ;

virtual void onTransition(QEvent *evt); — cette fonction, appelée quand la transition est déclenchée, doit être réimplémentée pour être personnalisée.

II-B-5 - QEventTransition

Bref rappel sur QObject (traduction adaptée d'un extrait de la documentation Qt) :

« La classe QObject est la classe de base de tous les objets Qt. Les QObject s'organisent en arborescence d'objets, ils peuvent recevoir des événements via event et filtrer les événements d'autres objets. Notez que la macro Q_OBJECT EST OBLIGATOIRE pour tout objet qui implémente des signaux, des slots ou des propriétés. Nous recommandons fortement l'utilisation de cette macro dans toutes les sous-classes de QObject, qu'elles utilisent ou non des signaux, des slots et des propriétés, car le fait de ne pas le faire peut conduire certaines fonctions à présenter un comportement étrange. »

Ceci pour attirer l'attention sur la possibilité de créer un QObject connecté sur un appareil extérieur à l'ordinateur, et lui transmettant des événements traités par le QObject.

La classe QEventTransition fournit une transition pour les événements Qt. Elle permet à des QObject de transmettre un changement d'état, tel l'objet bouge de la classe QPushButton :

QPushButton *bouge = new QToolButton("appuyez", this);
machine = new QStateMachine(this);
QState *etat = new QState(this, machine);
QEventTransition *transition = new QEventTransition(bouge, QEvent::Enter);
transition->setTargetState(... cible de la transition...);
etat->addTransition(transition);

Nous n'utiliserons pas cette classe, mais son héritière décrite ci-dessous.

II-B-6 - QKeyEventTransition

Classe permettant d'utiliser un événement « touche » comme déclencheur dans une ou plusieurs transitions pour communiquer avec les états simultanément ou individuellement.

L'appui sur la touche '6' déclenche la transition avance de l'état arret et provoque le départ du déplacement de l'objet en activant la fonction onEntry de l'état bouge :

TrTouchBouge *avance = new TrTouchBouge(this, QEvent::KeyPress, Qt::Key_6);

TrTouchBouge est une classe héritière de QKeyEventTransition. Nous indiquons à cette transition qu'elle sera active si la touche 6 du clavier est appuyée ;

avance→setTargetState(etatdeplacement); — l'état bouge est ajouté comme cible à l'objet de transition avance, pour que sa fonction onEntry soit déclenchée par cette transition ;

arret->addTransition(avance); — et cet objet de transition est, bien entendu, ajouté à l'état arret.

Les deux fonctions virtuelles principales de cette classe sont :

virtual bool eventTest(QEvent *evt); — cette fonction, déclenchée par l'appui de la touche, teste l'événement. Elle doit être réimplémentée pour être utilisée. Si elle retourne une valeur true, elle déclenche alors la transition et passe la main à la fonction onTransition. J'ajouterai « innocemment », qu'il est inutile de tester si c'est la touche 6, parce que c'est elle qui a enclenché la transition et que l'implémentation de cette fonction concerne des tests supplémentaires ;

virtual void onTransition(QEvent *evt); — cette fonction est appelée lorsque la transition est déclenchée, elle doit être réimplémentée pour personnaliser la transition.

II-B-7 - QMouseEventTransition

De même que QKeyEventTransition cette classe permet d'utiliser un événement émis par la souris comme déclencheur dans une ou plusieurs transitions pour inciter les états à communiquer entre eux simultanément ou individuellement.

Nous n'utiliserons pas cette classe dans notre apprentissage, mais son principe d'utilisation est similaire QKeyEventTransition.

III - Fenêtre principale : étape fenetre

Pour comprendre le fonctionnement de la structure de la machine à états, nous allons concevoir une simple surface graphique avec des objets animés : un objet sera piloté avec les touches du clavier numérique (ou celles que vous souhaiterez), ensuite nous ajouterons plusieurs autres objets qui circuleront indépendamment. Puisque cette application n'est utilisée que comme support pour un apprentissage, la fenêtre de l'application sera réduite au plus simple. Nous éviterons les menus déroulants standards.

Le visuel montre :

- une zone pour le graphisme ;

- sur un côté : les boutons de commandes générales ;

- en dessous une ligne prévue pour l'affichage des informations.

Étant donné la simplicité du bout de source du fichier main, je ne l'expose pas.

Dans la suite de ce livre, je définirai le nom des fonctions que je créerai de la manière suivante : la première lettre de chaque terme constituant ce nom sera en majuscule. Ceci pour les démarquer du texte, étant donné que j’utilise des termes en français non accentué pour les nommer ; par exemple : VaADroite(). Quant aux variables elles seront généralement libellées en minuscules, mais parfois elles contiendront une majuscule pour faciliter la compréhension par exemple : stopD au lieu de stopd. De même j’utiliserai des abréviations : « drt » pour droite et « gah » pour gauche.

III-A - La classe Fenetre

La classe Fenetre (habituellement nommée classe mainwindow dans la création d'un projet) contient les objets :

- scene : zone graphique où se situera l'animation. Cet objet sera associé ultérieurement à un objet de la classe Vue ;

- des boutons qui sont de deux types :

- démarrer et quitter. Ils seront accessibles pendant l'animation et bénéficieront d’un lien dans la « machine »,

- le bouton configuration qui ne sera actif qu’en dehors de l’activité graphique ;

- ligneinfo, zone d'édition en lecture, chargée d’afficher les messages, mais qui ne sera pas utilisée dans cet apprentissage.

Le diagramme des commandes, à ce stade, reste assez simple. À noter que l'objet de la classe Vue, quoique visible dans le diagramme, ne sera pas traité dans cette partie. Toutefois nous en créons dès maintenant une instance vide pour des raisons pratiques d’héritage d’objet, ce qui nous évitera de reprendre certaines instanciations, quand les sources seront plus compliqués.

Les instances des boutons sont créées dans la classe Fenetre et seront connectées dans la classe Scene. Le bouton « configuration » ouvrira une boite de dialogue pour stocker ou modifier les données dans un fichier externe, quant à la ligne d’information, elle ne sera pas connectée.

Pour faciliter la maintenance de la classe Fenetre, le contenu du constructeur est simplifié en faisant appel à des fonctions internes, cette classe hérite de QWidget qui, elle-même, hérite de QObject, ce qui est conforme à l’utilisation du Framework QStateMachine où l'animation et le traitement d’état ne s’appliquent que sur les objets de la classe QObject.

Remarques sur le fichier fenetre.h de la classe Fenetre :

...
public slots :
bool Configuration();
...
private :
Vue   *ptrvue;
Scene *pscene;
...

La déclaration de slot : Configuration est la fonction appelée par la boite de dialogue pour les données de configuration du logiciel. Cette fonction ne sera pas développée ici, puisqu'elle fait appel aux routines standards de gestion boite de dialogue Qt. Elle est présente pour montrer que, quand les objets animés circuleront dans la zone graphique, l’affichage de la boite de dialogue n’interrompra pas le déroulement des « machines » donc la circulation des objets.

Les pointeurs des instances de classe Scene et Vue sont déclarés à ce niveau pour qu'ils persistent jusqu'à l'arrêt du logiciel. Ils seront supprimés par le destructeur de l'instance. (Il en sera de même pour les autres pointeurs créés à ce niveau).

III-A-1 - Description du fichier source fenetre.cpp

Dans le constructeur la scène est initialisée avec une surface fixe qui est égale à la dimension de l'image de fond qui y sera affectée. Ces dimensions ne doivent pas être confondues avec les dimensions de la fenêtre définies par la fonction resize.

...
resize(1173, 904);
pscene = new Scene(0, 0, 1100, 850);
ptrvue = new Vue(pscene, this);
CreerLeMenu();
pscene->InitPtrVue(ptrvue);
pscene->InitialiserScene(nouveau, quitter, ligneinfo1);
...

L'initialisation de l'objet vue, contenant pscene, doit être déclarée avant la création du menu, car cette fonction contient la description des zones et a donc besoin de ptrvue pour définir la position des boutons et de la zone « info ». À la fin du constructeur, pscene est initialisé en lui envoyant les pointeurs des boutons.

Dans le destructeur :

delete pscene;
delete ptrvue;
...

Les pointeurs doivent être supprimés à l'arrêt du logiciel.

III-B - La classe ZoneAnime

Elle représente la classe qui gère : les séquences et la sauvegarde du test en cas d'interruption. Ces événements ne nous concerneront pas. Nous allons simplement utiliser la classe pour démarrer le test, et lancer la première séquence (qui est la seule dans ce manuel). La classe ZoneAnime est héritière de la classe état QState et contient son propre objet machine.

La réimplémentation de la fonction virtuelle onEntry redémarre une nouvelle séquence lorsque l'utilisateur clique sur le bouton nouveau. Son objectif est de nettoyer la scène pour y réinstaller les éléments à leur origine :

void ZoneAnime::onEntry(QEvent *)
{
if(machine)
{
machine->stop();
scene->NettoyerScene();
delete machine;
}
machine = new QStateMachine;
Sequence *sequence = new Sequence(scene, machine);
machine->setInitialState(sequence);
machine->start();
}

Dans la première partie, l'objet machine, s’il existe, est stoppé et détruit, la scène est nettoyée. Puis nous créons ou recréons l'objet machine que nous démarrons après lui avoir affecté un état sequence en tant qu'état initial.

La création de l'objet zoneanime se trouve dans la fonction InitialiserScene de la classe scene que nous étudierons plus loin. Voyons brièvement comment il sera instancié :

ZoneAnime *zoneanime = new ZoneAnime(this, machine);
zoneanime->addTransition(nouvellepartie, SIGNAL(clicked()), zoneanime);
...
machine->setInitialState(zoneanime);
machine->start();

Nous la connectons à l'aide d'une transition par le menu « nouvellepartie » ciblant zoneanime, et, avant de lancer l'objet machine, nous initialisons zoneanime comme état de départ.

Nous pouvons alors constater que scene et zoneanime contiennent chacun leur gestionnaire d'état « machine ». C'est lorsque ces gestionnaires sont démarrés, que leurs objets propriétaires ont un semblant d'autonomie.

III-C - La classe Sequence

Cette classe n'est utile que pour la réinitialisation de la scène, c'est pour cette raison que nous ne définirons qu'une séquence. Par contre, si plusieurs séquences étaient prévues, nous y chargerions les données concernant la position des composants formant la structure de la scène pour chacune de ces séquences. Pour cette fois, nous n'aurons donc qu'à traiter l'implémentation de la fonction onEntry qui est réduite à une ultime expression :

void Sequence::onEntry(QEvent *){ scene->ReInitialiserScene(); }

III-D - La classe Scene

La classe Scene qui hérite de la classe QGraphicsScene, contient les éléments graphiques qui s'afficheront dans la zone d'animation.

III-D-1 - Description du fichier source scene.cpp

Dans le constructeur, il n'y a que le chargement de l'image de fond. Ce serait ici que, par la suite, tous les objets graphiques ne possédant pas d'état, devraient être initialisés et dessinés sur la scène. L'image du fond est placée en dernière position de profondeur par l'instruction fondItem->setZValue(1); les autres objets devront avoir leur valeur Z > 1. Les paramètres de position et dimensions de la scène sont transmis directement par l'instruction QGraphicsScene(x , y, width, height).

Scene::Scene(int x, int y, int width, int height) : QGraphicsScene(x, y, width, height)
{
ObjetImage *fondItem = new ObjetImage( QString(":/motif/fond.png") );
fondItem->setZValue(1);
fondItem->setPos(0, 0);
addItem(fondItem);
}

Les dimensions de l'image du fond choisie sont de 904 pixels en hauteur sur 1173 pixels en largeur pour qu'elles coïncident au mieux avec les dimensions de la scène.

En tête de la fonction d'initialisation, nous créons trois instances :

- machine de la classe QStateMachine qui est la machine d'état de scene ;

- zoneanime qui hérite de QState ;

- final de la classe d'état QFinalState qui fait partie de la solution QStateMachine, et permet de fermer toutes les machines avant de sortir du logiciel.

void Scene::InitialiserScene(QToolButton *nouvellepartie, QtoolButton *quitterlejeu, QLineEdit *linformation)
{
QStateMachine *machine = new QStateMachine(this);
ZoneAnime *zoneanime = new ZoneAnime(this,  machine);
QFinalState *final = new QFinalState(machine);
zoneanime->addTransition(nouvellepartie, SIGNAL(clicked()), zoneanime);
zoneanime->addTransition(quitterlejeu, SIGNAL(clicked()), final);
connect(machine, SIGNAL(finished()), qApp, SLOT(quit()));
machine->setInitialState(zoneanime);
machine->start();
}

Les trois paramètres passés à la fonction sont affectés aux transitions entre la classe Scene et son état zoneanime par la fonction addTransition, cette fonction est membre de la classe QState dont hérite zoneanime. Ces transitions traitent les ordres depuis les boutons affichés sur la façade de la fenêtre.

L'action sur le bouton --nouvellepartie-- déclenchera la réinitialisation de la scène grâce à la fonction onEntry de l'état zoneanime\ qui réinitialisera la scène. L'action sur le bouton --quitterlejeu-- active l'état final de la classe QFinalState et termine le fonctionnement de l'application à l'aide de l'instruction de la connexion suivante qui, à l'émission du signal finished de la machine d'état déclenche le slot quit de l'application.

Attention : avant de lancer une machine d'état, il faut impérativement lui affecter un état initial. Ce qui est montré avec les deux dernières lignes.

IV - Déplacement dans une direction - étape 01

Les classes des objets graphiques dessinés sur la scène sont dérivées d'une même classe ObjetImage qui hérite de la classe de base QGraphicObject. L'arborescence des classes qui suit montre que l'image de fond est elle-même héritière directe de ObjetImage.

Nous n'utiliserons que les classes ObjetImage, ObjetMobile pour ce qui est des graphiques.

IV-A - Classe ObjetImage

Dans le fichier entête de cette classe deux constructeurs sont définis : un simple pour initialiser un pointeur dans un fichier entête, et le deuxième avec en paramètre le nom de fichier pour être directement instancié dans un fichier source. Il y a deux fonctions virtuelles : QRectF boundingRect et void paint, qui sont nécessaires à QGraphicObject pour dessiner ou effacer l'objet. L'image stockée est de la classe QPixmap qui est la plus appropriée, des quatre classes Qt concernant les images, pour être utilisée dans ce contexte :

class ObjetImage : public QGraphicsObject
{
public:
ObjetImage(QGraphicsItem* parent=0);
ObjetImage(const QString &nomfichier, QGraphicsItem *parent=0);
void InitImage(const QString &nomfich);
QSizeF size() const;
...
private:
QPixmap image;
}

Le fichier source ne présente pas de particularités, et n'a pas de lien direct avec la structure de la machine d'état.

IV-B - Classe ObjetMobile

Cette classe contient les fonctions pour le déplacement des graphiques mobiles, et, pour contrôler ce déplacement, nous utiliserons les chiffres du pavé numérique. Vous pouvez changer, mais il est préférable d'éviter le pavé des flèches :

int direction;
//0=fixe, gah=Qt::Key_4, drt=Qt::Key_6, hau=Qt::Key_8, bas=Qt::Key
void DirectionDroite();
void FixerObjet();
bool Immobile() const;
bool Mobile() const;
bool VaADroite() const;
bool NeVaPasADroite() const;

Pour récupérer ou fixer l'état de mobilité de l'objet, nous créerons des fonctions simples et compréhensibles. (Ci-dessus, le membre NeVaPasADroite aurait très bien pu être remplacé par une instruction !VaADroite.)

void BougeToi();
void Stop();
quint8 ValeurPas() const;
virtual void InitPas( quint8 vpas );

Le déplacement sera paramétré par une valeur de pas. Cette valeur pourrait être modifiée avec la configuration. Les variables de stockage de « direction » et « pas » sont privées, puisque cette classe les utilise en interne, et elles ne seront disponibles que par des fonctions. Les deux membres protégés, quant à eux, seront accessibles par la classe héritière.

Ci-dessous, on crée la variable machine qui sera la boucle d'état pour la classe héritière.

protected:
QVariantAnimation *animationdelobjet;
QStateMachine *machine;
private:
quint8 pas;
int direction;

Les objets mobiles ayant obligatoirement une variable de type QStateMachine, il est préférable de la placer dans la classe ObjetMobile et aussi d'y placer la variable QVariantAnimation qui va permettre le déplacement de l'objet.

La fonction BougeToi est appelée par la classe héritière au moment de l'initialisation. Lors de l'appel l'objet démarre la machine, pour notre cas l'instance de joueur machine->start();.

La fonction Stop, quant à elle, est appelée par la classe Scene pour cacher le joueur lors du nettoyage de la scène.

void ObjetMobile::Stop()
{
FixerObjet();
animationdelobjet->stop();
machine->stop();
}

L'objet est fixé pour réinitialiser la position. Si une animation est en cours, elle sera stoppée, et la machine de l'objet joueur sera arrêtée.

IV-C - Classe Joueur

Cette classe hérite de la classe ObjetMobile. Elle est pilotée par l'action sur les touches. Dans le cas étudié, il n'y a qu'un seul sens de déplacement, sans accélération, qui sera déclenché par la touche du clavier numérique 6 : chaque appui déplacera le joueur sur une petite distance vers la droite, un appui continu le déplacera sans s'arrêter jusqu'à la limite droite de la fenêtre où il s'immobilisera. Au démarrage du test, l'objet est fixe à gauche de la zone graphique.

En premier, je présente les fonctions simples :

void Joueur::ReInitialiser()
{
FixerObjet();
setPos(0, ( pvue->scene()->height() - size().height() )/2 );
show();
BougeToi();
}

Dans cette fonction, nous plaçons ou replaçons le joueur à la gauche de la scène, puis nous appelons BougeToi membre de la classe ObjetMobile pour démarrer la machine.

Le déplacement de l'objet lors de l'appui sur la touche est validé par la réalisation de trois tests : si l'objet est mobile, s'il va à droite et s'il n'a pas atteint la limite droite de la scène. Si ces trois conditions sont remplies, il se déplace de la distance assignée à la variable « pas » de ObjetMobile. La mise à jour de ce déplacement est effectuée par la fonction MajMouvement.

Un organigramme de la fonction sera plus explicite :

Si le déplacement est autorisé, nous calculons le point qu'il doit atteindre, nous testons à nouveau la limite, puis nous envoyons la valeur à l'instance d'animation qui redémarre.

void Joueur::MajMouvement()
{
animationdelobjet->stop();
if (Mobile())
{
if(VaADroite())
{
qreal tmpr = ( scene()->width() - size().width() );
if( x()<tmpr )
{
npos = pos();
npos.setX( x() + ValeurPas() );
if( npos.rx()>tmpr )
{
npos.setX(tmpr);
}
animationdelobjet->setEndValue(npos);
animationdelobjet->start();
}
}
}
}

On remarque qu'au début de la procédure, il y a un animationdelobjet->stop();. En effet, pour réinitialiser le point de fin, il faut stopper l'animation si elle est en cours. Quand l'animation est stoppée, l'objet reste au point où il est arrivé.

À noter : envoyer un ordre de stop à l'animation si elle est déjà fixée ne pose aucun problème.

Maintenant, passons à la partie complexe. Le diagramme qui suit décrit les interactions entre les états et l'exécution des opérations suite à l'entrée de l'objet dans l'un de ses états, après l'appui, soit de la touche 6 qui déclenche le déplacement, soit de la touche 5 qui interrompt un déplacement en cours :

Le symbole I encerclé, en haut à gauche de l'état arret désigne qu'il sera l'état initial au démarrage de la machine.

La transition avance qui appartient à l'état arret, a pour cible l'état bougeadroite. L'action sur la touche active sa fonction onEntry et lance la procédure de déplacement. De même, la transition stoppe appartient à bougeadroite et cible l’état arret, l'action sur la touche 5 active sa fonction onEntry et lance la procédure d'arrêt immédiat de l'objet joueur. Vous remarquerez aussi la présence d'une « transition par signal » dans la classe bougeadroite qui est la cible du signal finished de l'animation : en effet si aucune touche n'est saisie pendant le déplacement de l'objet, lorsque l'animation atteint son délai, elle émet le signal finished qui sera récupéré par l'état bougeadroite et envoyé comme transition stoppe.

Dans le fichier source de la classe Joueur c'est au niveau du constructeur que la création de la machine, les instanciations des états et les transitions entre ces états sont initialisées, ainsi que la partie animation dont nous étudierons la classe QAbstractAnimation au prochain paragraphe. Vous remarquerez que la machine n'est pas démarrée en fin de constructeur, parce que c'est l'initialisation dans la classe Scene qui doit provoquer le démarrage des machines de tous les graphiques mobiles.

Les quelques lignes du début du constructeur concernent l'initialisation des variables :

InitImage( QString( ":/motif/joueur.png" ) );
setFlags( QGraphicsItem::ItemIsFocusable );
setZValue(4);
InitPas(200);

- La valeur en Z pour les superpositions entre objets graphiques sur la scène.

- La valeur du pas : c'est le nombre de pixels de déplacement pendant l'animation.

Pour ce qui est de l'image « png » de l'objet joueur, j'ai choisi un motif de 70px de haut sur 50 de large.

Ensuite, nous créons le déroulement de l'animation qui sera appliquée à joueur lorsque nous appuierons sur la touche 6 :

animationdelobjet = new QPropertyAnimation(this,"pos"); — initialisation du pointeur de animationdelobjet de la classe QVariantAnimation, le pointeur « this » désigne l'objet joueur et le trigramme «´pos » indique que l'animation s'appliquera sur la propriété « position » de l'objet.

animationdelobjet->setDuration(500); — nous donnons une durée à l'animation, cette durée, en fonction de la valeur du pas exprimera une vitesse de déplacement.

machine = new QStateMachine(this); — initialisation du pointeur de l'objet machine. Comme nous sommes dans l'objet joueur, nous affectons immédiatement le pointeur de joueur « this » comme parent à machine. Plus loin, nous créerons la transition qui permettra à cette machine de lancer l'animation.

Nous assignons ci-dessous à joueur les états dont les classes seront décrites plus bas :

BougeADroite *etatdeplace = new BougeADroite(this, machine); — cette ligne crée une instance de la classe état BougeADroite.

Arret *arret = new Arret(this,machine); — création de l'instance de la classe état Arret. Cet état contient la transition réagissant à l'action sur la touche 6, qui fera basculer joueur de l'état arret à l'état bougeadroite.

Nous écrivons maintenant les lignes d'instructions pour la création des transitions. À cet instant, nous n'avons qu'une classe de transition TransTchBouge héritière de la classe QKeyEventTransition spécifique à l'événement touche du clavier. Nous étudions cette classe avant d'aller plus loin dans le constructeur :

TransTchBouge(Joueur *vjoueur, QEvent::Type t, int k); — dans le constructeur, le pointeur de l'objet joueur est passé en paramètre pour pouvoir y accéder depuis les fonctions de la transition. Le paramètre QEvent est le type d'événement déclencheur : ici « KeyPress ». « k » définit la touche du clavier activant l'événement, dans notre cas 5 ou 6.

Quand la transition est validée la fonction virtuelle utilisée onTransition est exécutée :

if (touche == Qt::Key_5)
joueur->FixerObjet();
else
joueur->DirectionDroite();

Nous la réimplémentons avec une instruction qui aiguille la touche appuyée vers l'action à entreprendre.

Nous aurions pu utiliser la fonction eventTest au lieu de onTransition, le résultat final aurait été identique, puisque la transition étant confirmée, elle active l'état ciblé. Mais avec cette option, nous n'aurions pas été conformes à la philosophie de QStateMachine qui doit être respectée le mieux possible dans un apprentissage. Autrement, la question doit être posée dans un développement courant en tenant compte du suivi de la maintenance.

Vous remarquerez aussi que je teste la valeur de la touche, ce qui est contraire à mon écrit sur la description des fonctions de QKeyEventTransition dans la partie précédente. La raison est que cette classe est volontairement utilisée pour créer les transitions des touches 6 et 5.

Nous reprenons notre constructeur de la classe Joueur aux lignes d'instructions des transitions :

- en premier le déclenchement du déplacement :

TransTchBouge *avance = new TransTchBouge(this, QEvent::KeyPress, Qt::Key_6); — création de la transition avance provoquée par la touche 6.

avance->setTargetState(etatdeplacement); — cible de la transition avance.

arret->addTransition(avance); — ajouter la transition à l'état arret qui sera le boutefeu ;

- en deuxième le déclenchement de l'arrêt :

TransTchBouge *arrete = new TransTchBouge(this, QEvent::KeyPress, Qt::Key_5); — création de la transition arrete provoquée par la touche 5.

arrete->setTargetState(arret); — cible de la transition arrete.

bougeadroite->addTransition(arrete); — ajouter la transition à l'état bougeadroite qui sera le déclencheur.

Sur le diagramme, nous avons remarqué la connexion entre le signal finished de l'animation et la classe état BougeADroite pour stopper l'objet quand le trajet de l'animation se finit sans intervention au clavier de 6 ou 5. Pour réaliser cette connexion, nous plaçons cette ligne :

bougeadroite->addTransition( animationdelobjet, SIGNAL(finished()), arret); — une transition ajoutée à l'état BougeADroite pour enclencher l'état Arret et stopper l'objet en fin d'animation.

Celle-ci n'est pas obligatoire puisque, quand l'objet atteint la limite, nous devons réinitialiser la séquence. Mais nous pourrions imaginer qu'une fois stoppé à la limite, l'objet se replacerait au début automatiquement : ce qui serait possible, grâce à ce : SIGNAL(finished()).

Pour terminer ce constructeur, nous devons indiquer l'état initial au démarrage, ou redémarrage, de machine : machine->setInitialState(arret); — l'objet machine place l'état arret comme état initial.

Évidemment dans le cas de ce simple déplacement, la classe Joueur est la plus importante partie au point de vue complexité et utilisation primaire des classes de la structure de machine d'états.

IV-D - État BougeADroite

Cette classe class BougeADroite : public QState n'a qu'un but : être la cible de la transition avance pour lancer la procédure de mouvement du joueur. Pour l'instant, elle est plutôt maigre, en sus du constructeur, elle ne contient que la fonction virtuelle :

void BougeADroite::onEntry(QEvent *)
{
joueur->MajMouvement();
}

Par cette fonction, on lance la mise à jour du déplacement du joueur.

IV-E - État Arret

Cette classe, tout aussi maigre que la précédente, ne contient que la réimplémentation de la fonction virtuelle :

void Arret::onEntry(QEvent *)
{
joueur->FixerObjet();
joueur->MajMouvement();
}

Avec cette fonction nous immobilisons le joueur en plaçant la valeur de direction à 0, puis nous lançons la mise à jour du déplacement, ce qui signifie que nous stoppons l'animation. Voir l'organigramme au début de cette section.

N'oublions pas que dans les classes d'état QState, la fonction onEntry est exécutée juste après la transition, et la fonction onExit lorsque nous quittons l'état. Dans l'exemple en cours, quand la transition est validée, la fonction onExit de la classe Arret est exécutée avant la fonction onEntry de la classe BougeADroite.

IV-F - Modification de la classe Scene

La classe scène réunit tous les objets de la zone graphique. Ces objets ne sont pas obligatoirement initialisés dans cette étape, mais la présence d'un pointeur les représentant est nécessaire. Notre objet devant apparaître dès le départ de l'application, nous créons son occurrence dans le constructeur :

...
addItem(fondItem);
joueur = new(Joueur);
joueur->hide();
addItem(joueur);
...

Hide() : cache l'objet en attendant l'initialisation, pour éviter des apparitions intempestives.

Puisque l'objet joueur existe tout au long de l'application, nous supprimerons son pointeur dynamique dans le membre destructeur : Scene::~Scene(){if(joueur) delete joueur;}.

Nous avons observé dans le chapitre précédent que la classe ZoneAnime était chargée de nettoyer la scène avant que celle-ci ne soit réinitialisée par la classe Sequence. Donc nous pouvons maintenant mettre à jour les deux fonctions membres de Scene, chargées de cette opération :

void Scene::NettoyerScene()
{
joueur->Stop();
joueur->hide();
joueur->setEnabled(true);
}

La fonction Stop a été décrite dans la classe Joueur.

void Scene::ReInitialiserScene()
{
setFocusItem(joueur, Qt::OtherFocusReason);
joueur->ReInitialiser();
}

La fonction ReInitialiser a été décrite dans la classe Joueur.

La fonction setFocusItem place le curseur (virtuellement) sur l'objet graphique. Si nous omettons cette instruction, il faudrait « cliquer » sur celui-ci au départ de la séquence pour que l'action sur les touches lui soit attribuée, sinon il resterait immobile.

IV-G - L’animation

La partie animation va être étudiée succinctement. Pour avoir des notions plus précises sur la classe QAbstractAnimation, il vous faudra consulter la documentation française de Qt et les exemples qui s'y réfèrent. Pour notre sujet, il nous suffit de connaître quelques classes pour gérer les déplacements des graphiques.

Notre schéma est simple : un objet est positionné, il subit un déplacement vers un autre point en suivant un segment de droite qui finit à ce point. Nous avons aussi la possibilité de l'arrêter en cours de déplacement. Pour réaliser ces événements, nous utiliserons les fonctions :

- start() : démarrer l'animation ;

- Stop() : arrêter l'animation ;

- setEndValue(x,y) : point d'arrivée ;

- SetDuration(500) : temps du déplacement en millisecondes.
Nous avons déjà créé l'objet de la classe QVariantAnimation dans le constructeur de Joueur qui subira cette animation Animationdelobjet = new QPropertyAnimation(this, "pos");.
Cette animation sera démarrée par le :animationdelobjet->start();dans la fonction MajMouvement de joueur, après avoir initialisé le point d'arrivéeanimationdelobjet->setEndValue(npos);dans cette même fonction.

L'arrêt de cette animation s'exécute au point d'arrivée, ou par le :

animationdelobjet->stop();de la fonction Stop de la classe ObjetMobile dont la classe Joueur hérite.

L'animation n'amène pas de concept compliqué dans notre apprentissage.

Nous en avons terminé avec ces premiers pas dans l’univers de QStateMachine pour le déplacement d’un objet dans un seul sens. Nous avons assimilé la base minimale de gestion des états et des transitions entre ces états, alors poursuivons l’apprentissage en augmentant les difficultés.

V - Déplacement dans deux directions contraires

Nous allons maintenant placer l'objet au centre de la scène et le déplacer dans le sens horizontal. La touche 6 déplace à droite et la touche 4 déplace à gauche, mais cette fois l'animation ne s'arrêtera qu'à la limite de la scène à moins que la touche enfoncée pendant l'animation soit inverse du sens de déplacement. Nous étudierons trois versions : la première dans laquelle nous effectuerons une première étape de modifications des classes facilement compréhensible, puis sa variante améliorée où nous aborderons la notion de partage de transitions et, pour finir, la version finale qui sera utilisée dans la suite de l’apprentissage.

V-A - Version de base

Dans ce premier diagramme simple, l'état initial de départ est arret. Celui-ci possède deux transitions qui vont servir à démarrer le joueur dans une des deux directions, suivant la touche appuyée.

Le déplacement est régi par deux états, bougeadroite et bougeagauche chacun d'eux possède une transition qui pointe vers son opposé pour inverser le sens de déplacement, si la touche de direction opposée est appuyée. À noter que nous ne traiterons pas le signal finished de l'animation, puisque la seule solution, quand l'objet est collé à la limite, est de repartir dans le sens inverse.

Au départ, seul l'état arret répondra à la sollicitation d'une des touches, ensuite il ne pourra plus y avoir qu'une alternative entre les deux autres états pour le sens de déplacement.

V-A-1 - Modification des classes existantes

La classe BougeADroite ne change pas.

V-A-1-a - Classe ObjetMobile

L'apport de la direction de déplacement à gauche impose deux nouvelles fonctions, la fonction pour imposer la direction à gauche, et une nouvelle fonction pour obtenir un code numérique de type entier pour représenter la direction :

void DirectionGauche();
quint8 CodeDirection();

Ce code sera utilisé pour la fonction MajDeplacement.

Pour le déplacement nous implémentons deux autres fonctions :

bool VaAGauche() const;
bool NeVaPasAGauche() const;

Le test NeVaPasAGauche aurait très bien pu être remplacé par une instruction !VaAGauche lors d’un test.

V-A-1-b - Classe Joueur

Avant d'étudier les modifications concernant les états, regardons le déplacement du joueur qui est maintenant continu jusqu'aux limites des bords droit et gauche de la fenêtre graphique. Pour une constance de la vitesse, nous la définissons par un rapport entre la largeur de la fenêtre et la durée de l'animation que nous imposons duree = 80;fixée dans le constructeur.

vitesse = scene()->width()/duree; — dans la fonction d'initialisation, nous calculerons la vitesse.

MajDeplacement() est totalement modifiée puisque nous calculons la distance restante entre l'objet et la limite de la fenêtre, en fonction de la direction, avant d'initialiser la nouvelle durée de l'animation avec setDuration :

Switch( CodeDirection() )
{
case Qt::Key_4 : //gauche
trajet = npos.x();
npos.setX(0);
break;
case Qt::Key_6 : //droite
trajet = scene()->width()-npos.x();
npos.setX( scene()->width() - size().width() );
break;
default :
bougeok = false;
}
if (bougeok)
{
animationdelobjet->setEndValue(npos);
animationdelobjet->setDuration( vitesse*trajet );
animationdelobjet->start();
}

Dans le constructeur de la classe Joueur, nous définissons les états de l'animation. Ils seront gérés par l'objet machine de la classe QStateMachine :

machine = new QStateMachine(this);.

Ensuite nous constituons une classe état Arret qui, dans la version actuelle, ne sera utilisée que pour démarrer l'animation et sera donc l'état initial de machine :

Arret *arret = new Arret(this, machine);
machine->setInitialState(arret);

L'animation sera pilotée par les deux objets de classe état BougeADroite et BougeAGauche :

BougeADroite *bougeadroite = new BougeADroite(this, machine);
BougeAGauche *bougeagauche = new BougeAGauche(this, machine);

Comme le montre le diagramme, les touches 4 et 6 sont connectées chacune à deux transitions qui déclencheront l'état opposé, suivant l'état en cours de l'objet joueur. Je ne décrirai ici que les transitions de la touche 4 :

TransTchStop *SversG = new TransTchStop(this, QEvent::KeyPress, Qt::Key_4);
SversG->setTargetState(bougeagauche);
arret->addTransition(SversG);

Cette transition, ajoutée à l'état arret et qui cible l'état bougeagauche provoque le déplacement vers la gauche.

Lorsque le joueur se déplace vers la droite, c'est-à-dire que son état bougeadroite est actif, l'appui sur la touche 4 déclenche l'inversion de la direction du trajet, nous définissons ce changement d'état dans le constructeur par les lignes suivantes :

TransTchInverse *DversG = new TransTchInverse(this, QEvent::KeyPress, Qt::Key_4);
DversG->setTargetState(bougeagauche);
bougeadroite->addTransition(DversG);

Cette transition est ajoutée à l'état bougeadroite et cible l'état bougeagauche.

Pour la touche 6 les actions seront inversées.

Dans cette version très simple, il est inutile de créer une transition du style :

bouge->addTransition(animationdelobjet, SIGNAL(finished()), arret);puisque les deux états du déplacement se ciblent mutuellement. Ajoutons la fonction qui sera sollicitée par les transitions pour inverser la direction :

void Joueur::InverserDeplacement()
{
switch( CodeDirection() )
{
case Qt::Key_4: // gauche
DirectionDroite();
break;
case Qt::Key_6: // droite
DirectionGauche();
break;
}
}

V-A-2 - Nouvelles classes

V-A-2-a - Classe BougeAGauche

Cette classe reprend le même schéma que la classe BougeADroite en sus du constructeur, elle ne contient que la fonction virtuelle onEntry :

void BougeAGauche::onEntry(QEvent *)
{
joueur->MajMouvement();
}

V-A-2-b - Classe TransDemarre

Cette nouvelle transition démarre l'objet joueur, soit en début de séquence, soit lorsqu'il est fixe à une limite de la fenêtre. Elle hérite de la classe QKeyEventTransition spécifique à l'événement touche du clavier. Le membre onTransition contient les instructions pour initialiser la direction suivant la touche appuyée et ordonner le démarrage de l'animation. L'état cible est déclenché sitôt la sortie de la fonction onTransition.

void TransDemarre::onTransition(QEvent *)
{
if(touche == Qt::Key_4)
joueur->DirectionGauche();
else
joueur->DirectionDroite();
}

Éventuellement nous aurions pu créer une classe de transition pour chacune des touches.

V-A-2-c - Classe TransInverse

Avec cette classe héritière de la base QKeyEventTransition et qui produit les transitions utilisées par les états bougeadroite et bougeagauche, nous allons pouvoir inverser le sens de déplacement pendant la durée du trajet de l'objet joueur. Au début de ce livre au chapitre ---petit rappel--- nous avons lu que cette classe de base avait deux membres principaux. Nous les implémenterons afin de mettre en évidence leur utilité :

- eventTest : est la fonction booléenne appelée au début de la transition pour la valider ;

- OnTransition : est une fonction qui s'exécute lorsque la transition est acceptée.

La fonction eventTest analyse si la touche enfoncée et son déplacement confirment le changement de l'état du joueur.

bool TransInverse::eventTest(QEvent *evt)
{
if( !QKeyEventTransition::eventTest(evt) )
return false;
return
(touche == Qt::Key_4 && joueur->VaADroite())
||
(touche == Qt::Key_6 && joueur->VaAGauche());
}

Si un des deux cas est approuvé, onTransition est appelé :

void TransInverse::onTransition(QEvent *)
{
joueur->InverserDeplacement();
}

Ce membre ne contient que l'ordre d'inversion de direction. La transition est exécutée et l'état ciblé activé.

V-B - Version améliorée - étape 02

Dans cette version, nous ajoutons la touche 5 qui va stopper le joueur pendant son déplacement. Le diagramme est légèrement modifié, il montre deux nouvelles transitions issues de la touche 5 et chacune pointe vers bougeadroite et bougeagauche. La cible de ces transitions sera l'état arret. Les modifications à effectuer étant simples, cette version ne constitue pas d’étape dans le projet Qt contenant les sources du cours.

Nous ajoutons les lignes suivantes dans le constructeur de la classe Joueur :

QKeyEventTransition *stopG = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_5);
bougeagauche->addTransition(stopG);
stopG->setTargetState(arret);
QKeyEventTransition *stopD = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_5);
bougeadroite->addTransition(stopD);
stopD->setTargetState(arret);

Nous utiliserons donc la classe de base QKeyEventTransition directement, sans créer de classe héritière puisque seule la touche 5 déclenche la transition et qu'aucune instruction particulière n'est prévue.

V-B-1 - Partage de transitions

Nous profiterons de la simplicité de cette version, pour étudier le partage des transitions entre plusieurs états. Dans la section précédente, l'appui de la touche 5 stoppe l'animation, quel que soit le déplacement. Mais nous avons dû créer deux objets de la classe QKeyEventTransition et les traiter pour chaque état, cependant, nous avons la possibilité de faire autrement en utilisant un « conteneur ». Le diagramme qui suit décrit le principe :

Tous les états ont comme parent l'état conteneur, la transition lui est ajoutée et cible l'état arret. Ce qui fait que l'appui sur la touche 5 activera directement l'état arret et donc interrompra l'état actif en cours.

...
machine = new QStateMachine(this);
QState *conteneur = new QState(machine);
Arret *arret = new Arret(this, conteneur);
conteneur->setInitialState(arret);
machine->setInitialState(conteneur);
QKeyEventTransition *stopGD = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_5);
conteneur->addTransition(stopGD);
stopGD->setTargetState(arret);
BougeADroite *bougeadroite = new BougeADroite(this, conteneur);
BougeAGauche *bougeagauche = new BougeAGauche(this, conteneur);
...
// le reste du code source ne change pas.

Dans un partage des transitions, chaque état enfant hérite des transitions de l'état parent. Nous observons donc que tout état intégré au groupe conteneur héritera de cette transition stop.

V-C - Version finale - étape 03

Avec la présence de la touche 5 pour stopper l'objet, nous allons compliquer la version en ajoutant la possibilité d'accélérer l'objet si nous appuyons plusieurs fois sur la touche du même sens que la direction où l'objet se déplace, et en freinant l'objet si la touche enfoncée est inverse du sens de déplacement et que l'objet est mobile, jusqu'à ce qu'il change de direction, si la vitesse devient nulle.

Le diagramme de la gestion de ces états devient compliqué. Nous aurons à initialiser huit instances de transition :

- accelg et acceld qui accélèrent l'objet ;

- freing et freind qui ralentissent l'objet ;

- DversG et GversD qui inversent la direction de l'objet au bout du freinage ;

- Depgah et Depdrt qui démarre l'objet lorsqu'il est à l'arrêt.

La touche enfoncée envoie un signal de transition à tous les états qui lui sont connectés, et seul l'état actif de l'objet graphique l'interprétera pour exécuter une action, si bien sûr une action est prévue.

Par exemple, si l'état en cours est arret et que l'utilisateur appuie sur la touche 4, les états bougeagauche et bougeadroite n'étant pas actif au moment de l'appui seront ignorés. Alors la transition depgah activera la procédure onEntry de l'état bougeagauche qui deviendra actif pour déplacer l'objet vers la gauche, et ce, jusqu'à ce que l'objet atteigne la limite gauche ou bien que l'utilisateur appuie sur une autre touche.

Vous remarquerez que nous adoptons le principe d'un état conteneur.

V-C-1 - Modification des classes existantes

Dans cette partie nous effectuerons les modifications des fichiers des classes existantes pour les adapter au déplacement dans les deux sens.

V-C-1-a - Classe ObjetMobile

La fonction FixerObjet devient virtual void FixerObjet();et sera redéfinie dans joueur, car seule cette classe possède une vitesse variable qui influe sur cette fonction.

V-C-1-b - Classe Joueur

Avant d'étudier les modifications concernant les états, regardons le déplacement du joueur qui est maintenant continu jusqu'aux limites des bords droit et gauche de la fenêtre graphique. Nous devons calculer la durée de déplacement suivant la distance restante entre l'objet et la limite pour initialiser l'animation animationdelobjet->setDuration(dureedepl);

Où la durée est calculée comme suit :

int vitesseaccel = Vitesse en cours * acceleration;
dureedepl = distance restante * coefresiste/vitesseaccel;

Pour générer cette durée, dans le fichier entête, nous ajoutons des variables où acceleration est un coefficient d'amplitude de la vitesse et coefresiste un paramètre pour adapter le résultat au matériel ordinateur :

private:
quint8 vitesse;
quint8 limitevitesse;
quint8 acceleration;
quint8 coefresiste;

et les fonctions publiques pour la piloter :

void    AnnulerVitesse();
void    AugmenterVitesse();
void    DiminuerVitesse();
void    VitesseInitiale();

quint8  Vitesse() const;
bool    VitesseNulle() const;
bool    VitessePasNulle() const;
bool    VitesseMinimale() const;

Ces modifications très simples se passent de commentaires.

La fonction FixerObjet est donc maintenant virtuelle :

void Joueur::FixerObjet()
{
AnnulerVitesse();
ObjetMobile::FixerObjet();
}

dès lors que nous plaçons la direction à zéro, par mesure de sécurité nous annulons aussi la vitesse.

Passons au constructeur de la classe. Nous ajoutons l'initialisation des variables pour le déplacement :

...
vitesse         =   0;
limitevitesse   =  10;
acceleration    =   7;
coefresiste     = 150;
...

Ces valeurs pourraient être modifiées par la boite de dialogue configuration.

Nous continuons par la partie initialisation de machine et nous utilisons le partage des transitions vers arret dans l’état conteneur, pour en éviter une multiplication :

machine = new QStateMachine(this);
QState *conteneur = new QState(machine);
Arret *arret = new Arret(this, conteneur);
conteneur->setInitialState(arret);
machine->setInitialState(conteneur);
BougeADroite *bougeadroite = new BougeADroite(this, conteneur);
BougeAGauche *bougeagauche = new BougeAGauche(this, conteneur);

Nous définissons les transitions ajoutées à conteneur qui ciblent arret, en commençant par la touche 5 qui stoppe le joueur :

QKeyEventTransition *stopGD = new QKeyEventTransition(this, Event::KeyPress, Qt::Key_5);
conteneur->addTransition(stopGD);
stopGD->setTargetState(arret);

Lorsque l'animation en mouvement atteint la limite de la scène, droite ou gauche, elle arrête l'objet automatiquement dans la fonction MajMouvement à l'aide des instructions :

animationdelobjet->stop();
if(Immobile()||VitesseNulle()) return;

Mais, dans ce cas, l'état de déplacement du joueur reste actif, alors que son graphique est bloqué. De ce fait, nous devons prévoir pour les deux états de déplacement, une transition qui sera déclenchée par le signal venant de la fonction finished de animationobjet pour activer l'état arret du joueur, nous écrirons conteneur->addTransition( animationdelobjet, SIGNAL(finished()), arret );. Cette ligne mutualise la sortie des deux états.

L'étude du diagramme montre que les touches 4 et 6 provoquent chacune trois transitions qui sont validées suivant l'état en cours de l'objet joueur. Nous étudierons la touche 4 qui suscite les événements suivants :

--- la transition démarre le joueur arrêté, elle est ajoutée à l'état arret et cible l'état bougeagauche :

TransDemarre *depgah = new TransDemarre(this, QEvent::KeyPress, Qt::Key_4);
arret->addTransition(depgah);
depgah->setTargetState(bougeagauche);

--- la transition accélère le joueur se déplaçant vers la gauche avec une vitesse inférieure à la limite maximum :

TransAccelere *accelG = new TransAccelere(this, QEvent::KeyPress, Qt::Key_4);
bougeagauche->addTransition(accelG);
accelG->setTargetState(bougeagauche);

Cette transition est interne à l'état bougeagauche. Son action sera d'augmenter la valeur de la vitesse jusque la valeur limite ;

--- la transition stoppe le joueur se déplaçant vers la droite à la vitesse minimum :

TransInverse *DversG = new TransInverse(this, QEvent::KeyPress, Qt::Key_4);
DversG->setTargetState(arret);
bougeadroite->addTransition(DversG) ;

Cette transition, ajoutée à l'état bougeadroite, cible l'état arret ;

--- la transition freine le joueur se déplaçant vers la droite avec une vitesse supérieure à 1 :

TransFrein *freinD = new TransFrein(this, QEvent::KeyPress, Qt::Key_4);
bougeadroite->addTransition(freinD);
freinD->setTargetState(bougeadroite);

Cette transition est interne à l'état bougeadroite. Elle réduira la valeur de la vitesse jusque la valeur 1.

V-C-1-b-i - Importance de l’ordre de l’ajout des transitions

IMPORTANT : nous nous arrêtons un instant sur ces deux dernières transitions. Jusqu'à présent, nous les empilions sans tenir compte de l'ordre dans lequel nous les ajoutions puisque celui-ci importait peu. Mais cette fois nous abordons le principe de la machine qui gère ces états. Imaginons-la comme une boucle qui testerait les transitions l'une après l'autre en suivant l'ordre de l'ajout. Dès qu'une d'entre elles est validée elle est traitée. Dans notre cas :

pour la transition freinD : si le test est valide, la vitesse est décrémentée jusqu'à la valeur 1, pas en dessous. Et l'état en cours reste actif. L'objet gardera la vitesse minimale.

Avec la transition DversG, la fonction eventTest réimplémentée nous offre un test plus élaboré :

return
(
(touche == Qt::Key_4 && joueur->VaADroite())
||
(touche == Qt::Key_6 && joueur->VaAGauche())
)
&&
joueur->VitesseMinimale();

Oùjoueur->VitesseMinimale retourne vrai si la vitesse du joueur est minimale.

Si ce test est confirmé, le sens de déplacement sera inversé : ce qui signifie que nous ne passerons pas par l'état arret pour changer le sens de déplacement.

Si nous inversons l'ordre des transitions, freinD puis DversG, cette deuxième transition ne sera jamais utilisée, puisque la vitesse lors de freinG sera toujours à la valeur 1. Il n'y aurait donc pas d'inversion de direction possible, à moins de stopper l'objet et de le redémarrer.

D'où l'importance de la position de ces transitions lorsque nous les additionnons à l'état.

Vous pourrez facilement le constater en modifiant le fichier source dans le projet « Etape03 » récupéré dans le dépôt.

V-C-1-c - TransInverse}

La fonction eventTest est enchérie du test sur la vitesse minimale, l'inversion de direction ne pouvant être réalisée que si le joueur a été ralenti au maximum.

return
(
(touche == Qt::Key_4 && joueur->VaADroite())
||
(touche == Qt::Key_6 && joueur->VaAGauche())
)
&& joueur->VitesseMinimale();

V-C-2 - Nouvelles classes

Dans ces classes, toutes héritières de QKeyEventTransition, seul le membre onTransition est implémenté, parce que c'est l'appui sur la touche qui les déclenche sans conteste.

V-C-2-a - TransDemarre

Attachée à la classe Arret, cette transition initialise la direction puis applique au joueur la vitesse minimale, ce qui démarre l'objet.

void TransDemarre::onTransition(QEvent *)
{
if( touche == Qt::Key_4 )
joueur->DirectionGauche();
else
joueur->DirectionDroite();

joueur->VitesseInitiale();
}

Pour me contredire sur ce que j'ai écrit en début de section, le test sur la touche pour initialiser la direction aurait pu être réalisé dans une réimplémentation de eventTest. Mais comme la transition doit se faire obligatoirement, il est inutile de surcharger la classe.

V-C-2-b - TransAccelere

Attachée aux classes de déplacement, cette transition incrémente la vitesse jusque la valeur limite.

void TransAccelere::onTransition(QEvent *)
{
joueur->AugmenterVitesse();
}

V-C-2-c - TransFrein

Attachée aux classes de déplacement, cette transition décrémente la vitesse jusque la valeur minimale.

void TransFrein::onTransition(QEvent * )
{
joueur->DiminuerVitesse();
}

Dans cette section, nous avons appris que les transitions pouvaient être partagées et qu'il fallait porter une grande attention dans l’ordre de l’ajout des transitions dans la machine.

VI - Déplacement dans les quatre directions - étape 04

Nous affrontons maintenant la multiplicité des transitions en ajoutant les directions vers le haut et vers le bas. Elles subissent, comme dans le chapitre précédent, le freinage lorsqu'une touche autre que celle de la direction du déplacement en cours est appuyée, et, l'accélération si la touche de même direction est sollicitée. Nous définirons quatre états de déplacement : à gauche, à droite, en haut et en bas, plus un état arrêt. Pour simplifier le diagramme, seuls l'état déplacement à gauche et l'état arrêt seront traités.

Nous avons huit transitions pour chaque état de déplacement, six d'entre elles sont dues aux actions sur les touches de déplacement contraires à la direction de l'état. Si l'objet va vers la gauche, l'appui sur les touches 6,2,8 va freiner le déplacement (ex. :freinGD) ou le stopper si l'objet est à la vitesse minimum (ex. :GversD). La touche 5 stoppe net le déplacement en cours quel que soit l'état.

Nous constatons que ce diagramme devient complexe et nous impose une débauche de transitions. Même en usant du partage des transitions, il resterait difficile à appréhender. Nous allons-nous simplifier la vie et utiliserons les transitions sans cible, et un mixage partage/sans-cible. Le diagramme ci-dessous nous propose cette solution :

Analysons en premier l'état arret : il est la cible de la transition arrete ajoutée à l'état « partage » conteneur dont hérite tous les états. De ce fait, quel que soit l'état en cours, une pression sur la touche 5 bloque net le joueur. Les transitions depgah, depdrt, depbas et dephat chacune affectée à une touche de déplacement démarrent l'animation.

Les quatre états de déplacement ayant la même structure, nous étudierons bougeagauche :

- la transition accelG, qui augmente la vitesse, est ce qu'on appelle une « transition sans cible », de ce fait sa validation ne provoque aucun changement d'état. Ce type de transition permet une opération interne à l'état ;

- la transition versG, chargée de changer le sens de déplacement quand la vitesse est minimale, sera partagée, donc ajoutée à conteneur. Quand l'objet circule à vitesse minimale, l'appui sur une touche dans un sens différent de la direction stoppera et redirigera le joueur dans le sens de la touche. Ceci signifie que, sans partage, il faudrait initialiser douze transitions supplémentaires. En imaginant utiliser les touches 1,3,7,9 pour des directions en biais cela déclencherait une profusion de transitions ;

- la transition freinG diminue la vitesse. Comme dans la description ci-dessus elle est partagée, mais en plus, sans cible parce qu'elle ne provoque pas de changement d'état.

Avant d'examiner les modifications de la classe Joueur épluchons les nouveautés.

VI-A - Nouvelles classes

Nous ne créons que deux nouvelles classes d'état qui reprennent le même schéma que BougeADroite et BougeAGauche. Dans leur fonction virtuelle onEntry, on lance la mise à jour du déplacement du joueur.

Classe BougeEnBas :

void BougeEnBas::onEntry(QEvent *)
{
joueur->MajMouvement();
}

Classe BougeEnHaut :

void BougeEnHaut::onEntry(QEvent *)
{
joueur->MajMouvement();
}

VI-B - Modification des classes existantes

VI-B-1 - TransAccelere

La fonction onTransition est différente du précédent chapitre :

void TransAccelere::onTransition(QEvent *)
{
joueur->AugmenterVitesse();
joueur->MajMouvement();
}

Nous remarquons la présence de la fonction MajMouvement.

Nous avons vu plus haut, que puisqu'il n'y a pas de cible, il n'y a pas de changement d'état. Ce qui fait que la transition n'activera pas la fonction onEntry d'un état. Il faut alors prévoir la mise à jour du déplacement pour adapter la vitesse. Mais si la vitesse maximale est atteinte, il est inutile que la transition soit validée : en conséquence nous implémentons la fonction eventTest de la façon suivante :

bool TransAccelere::eventTest(QEvent *evt)
{
if ( !QKeyEventTransition::eventTest(evt) )
return false;
return !joueur->VitesseMaximale();
}

VI-B-2 - TransInverse

Au chapitre précédent, nous avons abordé l'importance de l'ordre d'ajout des transitions dans les états d'où la déclaration des transitions TransInverse avant les transitions TransFrein. Cette classe garde la fonction eventTest pour valider si la vitesse est minimale, quant à onTransition, elle est modifiée parce que maintenant, il y a quatre sens possibles pour rediriger le joueur et, du fait qu'elle est « partagée », il faut initialiser correctement la direction :

void TransInverse::onTransition(QEvent *)
{
switch( touche )
{
case Qt::Key_4 : { joueur->DirectionGauche(); }break;
case Qt::Key_6 : { joueur->DirectionDroite(); }break;
case Qt::Key_8 : { joueur->DirectionHaut(); }break;
case Qt::Key_2 : { joueur->DirectionBas(); }break;
}
}

La mise à jour du mouvement se fera dans l'onEntry de l'état ciblé.

VI-B-3 - TransFrein

Cette classe de transition sera « sans cible », la transition n'activant pas la fonction onEntry d'un état, et « partagée » ses occurrences étant ajoutées à conteneur. Nous prévoyons la mise à jour du déplacement pour ajuster la vitesse.

void TransFrein::onTransition(QEvent *)
{
joueur->DiminuerVitesse();
joueur->MajMouvement();
}

Ici, inutile de prévoir une implémentation d'eventTest, car les initialisations des objets TransInverse sont déclarées avant. Donc le test sur la vitesse minimale, s'il est validé, élude cette transition.

VI-B-4 - TransDemarre

Cette classe de transition démarre le joueur au début de la séquence ou s'il a été bloqué par une limite de la zone :

void TransDemarre::onTransition(QEvent *)
{
switch( touche )
{
case Qt::Key_4 :{ joueur->DirectionGauche(); }break;
case Qt::Key_6 :{ joueur->DirectionDroite(); }break;
case Qt::Key_8 :{ joueur->DirectionHaut(); }break;
case Qt::Key_2 :{ joueur->DirectionBas(); }break;
}
joueur->VitesseInitiale();
}

Notons que la transition de démarrage applique la vitesse initiale au joueur.

VI-B-5 - ObjetMobile

Nous créons de nouvelles fonctions pour initialiser les directions Haut et Bas et tester le sens déplacement :

void DirectionHaut();
void DirectionBas();
bool VaEnHaut() const;
bool VaEnBas() const;
bool NeVaPasEnHaut() const;
bool NeVaPasEnBas() const;

VI-B-6 - Joueur

Nous utilisons l'état de partage conteneur pour réunir les états que nous allons définir dans le constructeur de la classe. Tous les états seront apparentés à conteneur de ce fait l'initialisation de l'état de démarrage sera modifiée.

machine = new QStateMachine(this);
QState *conteneur = new QState(machine);
Arret *arret = new Arret(this, conteneur);
// Il faut déclarer l'objet arret avant l'initialisation.
conteneur->setInitialState(arret);
machine->setInitialState(conteneur);

Nous énonçons les transitions, affectées à l'état conteneur, qui concernent l'arrêt du joueur :

- l'événement touche 5 stoppe l'objet :

QKeyEventTransition *arrete = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_5);
conteneur->addTransition(arrete);
arrete->setTargetState(arret);

- l'arrêt imposé par l'animation quand le joueur atteint la limite de la scène, déclenché par le signal venant de la fonction finished :

conteneur->addTransition(animationdelobjet, SIGNAL(finished()), arret);.

Création des instances d'états du déplacement :

BougeADroite *bougeadroite = new BougeADroite(this, conteneur);
BougeAGauche *bougeagauche = new BougeAGauche(this, conteneur);
BougeEnHaut  *bougeenhaut  = new BougeEnHaut(this, conteneur);
BougeEnBas   *bougeenbas   = new BougeEnBas(this, conteneur);

Pour détailler le reste des transitions je ne traiterai que l'objet de l'état bougeadroite les quatre autres étant similaires :

- la transition qui démarre le joueur n'a pas à être partagée puisque nous ne définissons qu'une seule transition par état. Par ailleurs, nous n'y trouverions aucun gain, mais au contraire des soucis d'interférences avec les autres transitions dues à la position parmi l'empilement dans la machine.

TransDemarre *deplaceD = new TransTchBouge(this, QEvent::KeyPress, Qt::Key_6);
deplaceD->setTargetState(bougeadroite);
arret->addTransition(deplaceD);

- la transition qui accélère le joueur se déplaçant vers la droite :

TransAccelere *accelD = new TransTchBouge(this, QEvent::KeyPress, Qt::Key_6);
bougeadroite->addTransition(accelD);

Cette transition « sans cible », interne à l'état, augmente la valeur de la vitesse jusqu'à la limite maximum.

Étudions maintenant les transitions provoquées par les touches de direction différente. Pour chacune d'elles, il y aura une transition de la classe TransFrein chargée de freiner le joueur tant que sa vitesse est supérieure à 1 et une transition de la classe TransInverse qui va rediriger le joueur dans une autre direction si sa vitesse est égale à 1. Nous avons déjà exploré l'importance de la position des déclarations de transitions. Ici il faut que les transitions TransInverse soient signifiées avant les TransFrein sinon le joueur atteindrait la vitesse 1 et ne changerait jamais de direction :

- la transition TransInverse est partagée :

TransInverse *versG = new TransInverse(this, QEvent::KeyPress, Qt::Key_4);
conteneur->addTransition(versG);
versG->setTargetState(bougeagauche);

- la transitionTransFrein est « sans cible » ET partagée  :

TransFrein *freinG = new TransFrein(this, QEvent::KeyPress, Qt::Key_4);
conteneur->addTransition(freinG);

La fonction MajMouvement est modifiée pour traiter les mouvements haut et bas en modifiant la valeur de la position en Y du joueur.

Dans ce chapitre nous avons appris qu'une transition pouvait être sans cible et, de ce fait, ne pas changer l'état auquel elle est attachée, mais agir sur cet état. Nous savons aussi maintenant qu'une transition peut être partagée et sans cible.

VII - Objet non contrôlé

La classe QAbstractAnimation de Qt nous permet d'animer facilement les objets, nous pourrions donc en conclure que ce chapitre pourrait se passer de QStateMachine, mais le but, ici, est de préparer le terrain pour la suite de l'apprentissage où un objet se déplace sans contrôle et poursuivra un objet contrôlé par l'utilisateur.

VII-A - Déplacement d’un objet libre - étape 05

Cette fois le mouvement est perpétuel, nous n'avons donc pas de déclencheur. Seul l'appui sur le bouton « nouveau » réinitialise l'objet et recommence le mouvement.

Le diagramme montre que l'état avance est déclaré comme initial au début de la machine, ce qui signifie que sa fonction onEntry démarre l'animation dès que l'ordre machine->start()est donné.

Nous aurons donc, au démarrage de l'application, l'objet qui se déplace et lorsque l'animation se termine, elle envoie un signal à la transition reprend de l'état avance, qui déclenche alors l'état arret. Ce dernier, par sa fonction onEntry, replace les coordonnées de départ de l'objet à leur origine, et envoie un signal RePositionner. Celui-ci est capté par l'état avance qui actionne la reprise de l'animation.

VII-A-1 - La classe Predateur

Nous créons une nouvelle classe Predateur, elle hérite de ObjetMobile. Comme l’utilisateur n’intervient pas dans le déplacement de l’objet créé par cette classe, nous devons lui procurer un point de départ et un point d’arrivée. La suite de cette étape étant de créer plusieurs occurrences de Predateur, nous stockerons ces valeurs dans l’entête de la classe :

private:
...
QPointF     pntfin;
QPointF     pntdepart;

Ces valeurs seront définies dans la fonction Initialiser :

void Predateur::Initialiser()
{
hide();
pntfin = QPointF( ( scene()->width() - size().width() ), 0);
pntdepart = QPointF(0, (scene()->height() - size().height()) );
setPos(pntdepart);
show();
}

Nous imposons un déplacement du coin bas gauche au haut droit de la fenêtre graphique.

Dans le constructeur nous augmentons la valeur de la durée de l’animation pour voir un déplacement appréciable :

animationdelobjet = new QPropertyAnimation(this, "pos");
animationdelobjet->setDuration(4000);

La suite du constructeur initialise les états. Cette fois nous ne créerons pas de conteneur, les deux états seront affiliés à machine qui sera leur parent :

machine = new QStateMachine(this);
DeplacePrd *avance = new DeplacePrd(this, machine);
ArretPrd *arrete = new ArretPrd(this, machine);

Dans la classe Joueur les transitions étaient réalisées par l'appui sur les touches, donc à la volonté de l'utilisateur. Cette fois, deux transitions sont déclenchées par les états eux-mêmes, de façon à avoir une perpétuation dans le mouvement :

- la première transition est déclenchée par la fin de l'animation, elle appartient à avance et cible l’état arret.

QSignalTransition *reprend = new QSignalTransition(animationdelobjet, SIGNAL(finished()) );
avance->addTransition(reprend);
reprend->setTargetState(arret);

- la deuxième transition déclenche un signal de l'état arret, à destination de avance.arrete->addTransition(arrete, SIGNAL(RePositionner()), avance);.

Étant donné que nous souhaitons provoquer l'animation aussitôt le démarrage de l'application, pour ce faire, il suffit d'initialiser la machine avec avance :

machine->setInitialState(avance);et, comme nous le verrons un peu plus loin, c'est la classe Scene qui démarre la machine.

VII-A-2 - L’état DeplacePrd

Dans cette classe d'état nous réimplémentons la fonction onEntry qui sera chargée de déplacer l'objet :

void DeplacePrd::onEntry(QEvent *)
{
predateur->MajMouvement();
}

VII-A-3 - L’état ArretPrd

L'entête de cette classe d'état doit contenir le signal pour replacer l’objet predateur :

...
signals :
void RePositionner();
...

et son source voit sa fonction onEntry réimplémentée de cette façon :

void ArretPrd::onEntry(QEvent *)
{
predateur->Initialiser();
emit RePositionner();
}

Les positions de l'objet sont réinitialisées pour replacer le graphique de l'image, puis le signal est envoyé par emit.

Précisons que la réinitialisation de la valeur des points départ et arrivée à chaque reprise du mouvement, n'est pas nécessaire dans le cas étudié, puisque ces valeurs ne changent pas.

VII-A-4 - Modification de la classe Scene

Cette classe étant le contenant de tous les objets graphiques, nous ajoutons dans l'entête le pointeur sur l'objet Predateur et une fonction pour récupérer ce pointeur.

Dans le fichier source, au niveau du constructeur le pointeur est initialisé :

...
predateur = new(Predateur);
predateur->hide();
addItem(predateur);

On constate que l'objet predateur suit les mêmes instructions que l'objet joueur.

La classe Scene pilote l'objet machine et, de ce fait, l'animation. Elle l'arrête dans la fonction NettoyerScene et le démarre dans sa fonction ReInitialiserScene :

void Scene::NettoyerScene()
{
predateur->Stop();
predateur->hide();
predateur->setEnabled(true);
}
void Scene::ReInitialiserScene()
{
setFocusItem(predateur, Qt::OtherFocusReason);
predateur->Initialiser();
predateur->BougeToi();
}

L'instruction Initialiser replace les coordonnées à l'origine et redessine le graphe par un setpos, et l’instruction BougeToi redémarre la machine. C'est pour cette raison que, durant l'animation, un clic sur le bouton nouveau interrompt la machine et, en conséquence l'animation de la scène, puis reprend un nouveau cycle.

À l'exécution du programme, nous obtenons un objet animé indépendant.

Si nous modifions la dimension de la fenêtre de l'application, les points du trajet de l’objet restent sur les limites de la scène. Ceci est logique puisque les dimensions de cette scène sont calquées sur celles de l'image qui sert de fond. Pour avoir plus de liberté, il suffirait de placer une image, donc une scène, de plus grande taille (dépassant même les limites de l'écran) et d'utiliser les dimensions de la vue pour les trajets des objets.

Pour faire varier ce trajet, nous pourrions aussi implémenter un algorithme simple pour modifier, à chaque reprise, la position de départ et d'arrivée dans la fonction d'initialisation la valeur de ces points. Mais ce n'est pas le sujet de cet apprentissage.

VII-B - Série d’objets libres - étape 06

Pour tester la fiabilité de ce procédé, nous allons créer plusieurs objets de la classe Predateur :

VII-B-1 - Modification de la classe Predateur

Puisque nous créerons plusieurs occurrences objets de cette classe, nous la modifierons en initialisant des paires « points de départ et d'arrivée » pour obtenir des trajectoires différentes. En pratique, nous stockons des valeurs d'origine dans chaque objet et nous réinitialisons les variables utilisées dans l'animation quand c'est nécessaire, pour réaliser cela nous ajoutons une fonction ReInitialiser et modifions la fonction originelle Initialiser.

Dans le fichier d’entête :

ajoutons des variables de stockage des points départ et arrivée.

private:
Vue     *pvue;
QPointF  pntdepart, pntfin, pntfinsvd, pntdepsvd;

dans le fichier source, nous stockons les valeurs d'origine avec Initialiser :

void Predateur::Initialiser(const QPointF& pdep, const QPointF& pfin)
pntfinsvd = pfin;
pntdepsvd = pdep;

et nous définissons une fonction pour réinitialiser les valeurs d'origine lors de la reprise de l'animation :

void Predateur::ReInitialiser()
{
hide();
pntfin = pntfinsvd;
pntdepart = pntdepsvd;
...
}

VII-B-2 - Modification de la classe Scene

Dans l'entête de la classe Scene, nous modifions le pointeur objet par une liste de pointeurs.

...
private:
QList<Predateur*> lstpred;
…

Dans le source de Scene, les pointeurs sur les objets seront traités par boucle dans le constructeur :

...
for (int i=0; i< $nombre de predateur$ ;++i)
{
lstpred.append(new(Predateur));
lstpred.last()->hide();
addItem(lstpred.last());
}
...

Ainsi que dans la fonction de réinitialisation :

void Scene::ReInitialiserScene()
{
…
for(int i=0; i<lstpred.size(); ++i)
{
predateur->lstpred.at(i)->ReInitialiser();
}
}

Le principe des actions de changement d'états des objets Predateur sera plus aisément constaté si nous appliquons des vitesses différentes à chacun d'eux en changeant la valeur duration de la classe QVariantAnimation. Pour faciliter leurs initialisations qui suivront des trajets différents, nous ajoutons la fonction IniPredateur en fin du source scene.cpp.

void Scene::IniPredateur()
{
if(!lstpred.isEmpty())
{
int lp = lstpred.first()->size().width();
...
lstpred.first()→Initialiser( QPointF(-lp,-hp), QPointF(lg/2,hg) );
lstpred.first()->InitPas(1000);
...
}
}

Nous changeons la fonction virtuelle InitPas de la classe Predateur en la déclarant dans l'entête, et en insérant dans le source :

void Predateur::InitPas(int vpas)
{
animationdelobjet->setDuration(vpas);
}

Dès le lancement du programme, les objets circulent dans l’espace graphique.

Ce chapitre nous a permis d'appréhender un semblant d'automatisme dans un objet indépendant, mais pour le moment son comportement est imperturbable et ne peut être modifié que par l'arrêt du logiciel.

VIII - Objet contrôlé et objet libre - étape 07

Dans ce chapitre, nous animerons un objet contrôlé par l'utilisateur et un autre démarré dès le lancement du logiciel. Pour réaliser cette démarche, nous « piocherons » dans les chapitres précédents, nous utiliserons la structure et les fichiers du déplacement dans les quatre directions et y ajouterons les fichiers de la classe Predateur.

Dans le diagramme simplifié qui suit, la classe Scene contient deux objets : predateur dont la seule transition interne est provoquée par la sortie des limites, ce qui automatise son déplacement par une réinitialisation, et joueur, qui est piloté par ses transitions externes de la classe QKeyEventTransition qui le relie aux touches.

L'objet contrôlé qui est fourni par la classe Joueur, quant à l’objet libre, il est fourni par la classe Predateur vue au chapitre précédent. Ces objets ne subissant aucune modification, nous passons directement à la classe Scene.

VIII-A - La classe Scene

Dans cette classe nous regroupons dans l’entête les pointeurs sur l’objet predateur et l’objet joueur et les fonctions pour récupérer ces valeurs.

Joueur *pJoueur() const;
Predateur *pPredateur() const;
...
private :
Joueur *joueur;
Predateur *predateur;

Dans le fichier source, au niveau du constructeur, ils seront initialisés :

joueur = new(joueur);
joueur->hide();
addItem(joueur);
predateur = new(Predateur);
predateur->hide();
addItem(predateur);

- dans un premier temps, au moment de l’initialisation de la scène, nous initialisons les valeurs origine des points pour l’objet libre :

void Scene::InitialiserScene(...)
{
...
predateur->Initialiser(QPointF(...),QPointF(...));
QStateMachine *machine = new QStateMachine(this);
Jeu *jeu = new Jeu(this, machine);
...
}

- dans un deuxième temps, nous modifions la fonction de réinitialisation de la scène. Rappelons-nous que l’objet scene est créé par la Classe Fenetre d’où la fonction InitialiserScene. Et que c’est la classe Sequence qui appellera ReInitialiserScene pour recommencer la scène.

void Scene::ReInitialiserScene()
{
setFocusItem(joueur,Qt::OtherFocusReason);
joueur->ReInitialiser();
predateur->ReInitialiser();
}

N’oublions pas que l’action sur les touches s’applique sur l’objet désigné par setFocusItem.

Au démarrage du programme, l’objet libre se déplace suivant le trajet déterminé, et l’objet contrôlé, suivant le choix de l’utilisateur.

La suite de cette étude va nous montrer comment l’objet libre objet pourra changer d'attitude parce que nous lui imposerons un objectif qui l'obligera à modifier son trajet.

IX - L’objet contrôlé est une cible – étape 08

Précédemment nous avions établi que, lorsque l'objet predateur atteignait la fin de son trajet, il émettait le signal finished de l'animation, ce qui activait l'état arretprd pour réinitialiser l'objet et redémarrer son animation en activant son état DeplacePrd. À présent, l'objet libre n'atteindra jamais une des limites de la zone puisqu'il doit poursuivre l'objet que nous piloterons, sa classe Predateur doit connaître la position de cet objet de façon à se déplacer en modifiant sa trajectoire en fonction de cette information.

Au niveau de l'état ArretPrd de l’objet predateur nous établissons une liaison avec l’objet joueur afin de récupérer sa position pour nous permettre de calculer la nouvelle direction du trajet de predateur, puis d'émettre le signal RePositionner pour passer à l'état DeplacePrd. Dans le diagramme, le schéma de joueur est réduit au minimum parce que sa classe ne sera pas modifiée.

IX-A - Modification de la classe Scene

Dans cette classe, la fonction Inipredateur créée à la section précédente se résume à : predateur->Initialiser(QPointF(0,0));.

IX-B - Modification de la classe ObjetMobile

Cette classe est modifiée, car tous les objets mobiles ont, logiquement, des points départ et arrivée, nous déclarons les variables stockant ces points dans la section protected de cette classe pour qu'elles soient sécurisées, mais facilement disponibles par les classes héritières. Le fichier entête devient :

protected:
...
QStateMachine *machine;
QPointF pntdep, pntfin;
void PointCible(const QPointF cible);

y figure aussi la déclaration de la fonction calculant le point cible, en rapport avec le pas de l'objet suiveur. Par convention celui-ci, ayant une vitesse constante déterminée par la valeur de son pas et la durée de l'animation, suivra une trajectoire « axe suiveur/cible » sur un segment égal à la valeur de son pas. À chaque fin d'animation, c'est-à-dire quand le suiveur atteint son point final, la position de la cible est collectée pour ajuster la nouvelle trajectoire. Mais en analysant le schéma, nous observons que pendant la durée du déplacement du suiveur en direction de la cible, celle-ci change de position, ce que le suiveur ignore puisqu'il est en cours de déplacement.

Nous constatons que les éléments en position -A- au début, se trouvent en position -B- à la fin de la durée du déplacement du suiveur. Nous en déduisons que si la durée est longue la position de la cible pourrait être beaucoup plus éloignée, nous devons alors privilégier une durée courte avec un pas minimum afin de provoquer un rajustement de la trajectoire plus fréquent. Pour le détail du calcul du nouveau point de fin de déplacement, voir le code source du fichier « objetmobile.cpp ».

Nous terminerons en ajoutant un traitement consécutif à la collision de l'objet suiveur avec la cible. Ce traitement sera de replacer l'objet suiveur à son origine et de reprendre la poursuite, la cible continuant son trajet. La solution que nous mettrons en œuvre consiste à manipuler un état supplémentaire avec la directive « Signals » des classes Qt. Une nouvelle classe de transition nous apparaît, elle aura comme fonction d'effectuer la transition entre l'état DeplacePrd et l'état ArretPrd en renvoyant le suiveur à sa position d'origine. Nous l'examinerons ultérieurement :

IX-C - Modification de la classe Predateur

Dans l'entête, le constructeur est modifié pour communiquer à la classe le pointeur de l'objet joueur :

class Predateur : public ObjetMobile
{
public:
Predateur(Joueur *valptr);
...
void PositionCible();
virtual bool TestCollision();

private:
...
Joueur  *pjoueur;
…

Nous ajoutons une fonction pour mettre à jour le point final du trajet qui cible la position du joueur, plus une fonction virtuelle pour tester la collision, et un pointeur sur l'objet joueur est stocké en « private ».

Les précédentes déclarations de variables :QPointF pntdepart, pntfin, pntfinsvd, pntdepsvd;ont été déplacées vers la classe parent Objetmobile que nous avons étudiée précédemment.

Dans le constructeur du fichier source, nous ajoutons une instance de la classe TrContact héritière de QSignalTransition exposée plus loin :

...
TrContact *dedans = new TrContact(this, avance);
avance->addTransition(dedans);
dedans->setTargetState(arrete);
...

La fonction void Predateur::PositionCible(){ PointCible(pjoueur->pos());nous retourne la position de la cible.

Contrairement au trajet du chapitre précédent, le point de fin étant variable, il n'est pas initialisé au début du jeu. C'est la classe Predateur qui initialisera ce point final. Dans le fichier source, la fonction Initialiser est donc modifiée pour fournir uniquement le point de départ, elle devient :

void Predateur::Initialiser(const QPointF& pdep)
{
pntdep=pdep; pntfin=pdep;
pntfin.setX( pntdep.rx()+1 );
...
}

toutefois pntfin est initialisé à ce niveau « par prudence ».

Puisque l'objet suiveur ne sera plus interrompu dans son déplacement, la mise à jour de son animation n'a plus à comporter de setStartvalue, cette instruction est validée au début du jeu et devient ensuite inutile puisqu'il suffit de changer le point final au fur et à mesure du trajet. La fonction MajMouvement devient :

void Predateur::MajMouvement()
{
animationdelobjet->setEndValue(pntfin);
animationdelobjet->start();
}

Quant au start, il est toujours nécessaire.

Et pour en terminer avec les nouvelles fonctions du source, nous implémentons celle qui recherche la collision entre l’objet predateur et la cible :

bool Predateur::TestCollision()
{
QList<QGraphicsItem*> lstobjet = collidingItems( Qt::IntersectsItemBoundingRect );
for(int i=0; i<lstobjet.count(); i++)
{
if( (ObjetImage*)pjoueur == (ObjetImage*)lstobjet.at(i) ) return true;
}
return false;
}

La fonction collidingItems nous renvoie tous les éléments graphiques en collision avec l'objet predateur. Le paramètre IntersectsItemBoundingRect définit comme contact tout item en intersection avec le rectangle de délimitation de l'élément. Ensuite, s'il y a une liste en retour, il suffit d'y chercher l'existence de la valeur du pointeur de la cible.

IX-D - État ArretPrd

Comme précisé en début de cette section, le signal finished provoque l'activation de l'état ArretPrd pour réinitialiser la fin du trajet de l'objet. La fonction onEntry de cette classe est modifiée :

void ArretPrd::onEntry(QEvent *)
{
predateur->PositionCible();
emit RePositionner();
}

Le signal RePositionner est activé.

L'état provoque chez l'objet predateur la saisie de la nouvelle position de la cible et active le signal. Ce sera la seule modification dans cette classe.

IX-E - État DeplacePrd

Il est logique que c'est pendant le déplacement que s'effectuera le test de collision entre le suiveur et la cible. S'il y a contact, la classe DeplacePrd activera son signal Percussion. Dans l'entête nous insérons la directive d'implémentation du signal.

...
~DeplacePrd();
signals :
void Percussion();
protected:
...

Dans le source, la fonction onEntry est modifiée pour le test de contact.

void DeplacePrd::onEntry(QEvent *)
{
if(predateur->TestCollision())
emit Percussion();
else
predateur->MajMouvement();
}

Dans les faits la fonction onEntry, appelée quand l'état est activé, démarre le déplacement qui sera ensuite géré par l’objet animationdelobjet. Il serait donc plus adéquat de réaliser le test à l'intérieur de cet objet, mais cela ne nous permettrait pas de manipuler les transitions avec la directive « signals » comme nous le verrons ci-dessous.

IX-F - Classe TrContact

\section*{Créer d'}

Dans la perspective d'utiliser les classes du framework, nous bâtissons une classe héritière de QSignalTransition pour effectuer la transition entre l'état ArretPrd et l'état DeplacePrd lorsque ce dernier émet son signal Percussion.

La classe est du type : class TrContact : public QSignalTransition. La connexion du signal est établie dans le constructeur du source :

TrContact::TrContact(Predateur *vpredat, DeplacePrd *vdepl) : QSignalTransition( vdepl, SIGNAL(Percussion()) )
{
predateur = vpredat;
}

Le pointeur de DeplacePrd est transmis au paramètre QObject*sender de QSignalTransition.

Lorsque la transition est requise, la fonction réimplémentée onTransition est exécutée :

void TrContact::onTransition(QEvent *)
{
predateur->Initialiser(QPointF(0,0));
}

Nous avons étudié les états qui émettent des signaux, et des transitions qui répondent à ces signaux. Jusqu'à présent les objets sont créés au départ du logiciel et disparaissent à l'extinction du programme. Dans la mesure où nous avons en main tout ce qu'il faut savoir pour créer un objet avec un événement et le détruire, nous allons le faire dans les chapitres qui suivent…

X - Un objet produit un autre objet

X-A - Démarrer un objet - étape 09

Nous ajoutons à l’objet scene un objet de la classe Projectile qui sera déclenché par l’objet joueur, et dont la direction suivra le sens du déplacement. Nous utiliserons la classe de base QSignalTransition pour la transmission des informations entre états d'objets différents, et, pour la recherche d'un contact entre Projectile et Predateur, nous recourrons au signal valueChanged émis par l'animation :

Pour clarifier le diagramme ci-dessous, j'ai masqué les états précédemment définis de la classe Joueur puisqu'ils font partie d'un seul état Conteneur et qu'ils ne seront pas modifiés, seule sa nouvelle transition « envoyer » est indiquée. De même pour la classe Predateur qui reste inchangée :

Au démarrage de la séquence nos deux objets familiers se comportent comme auparavant, quant au nouvel objet projectile, il est initialisé à son état Arrete. L'ordre de tir est déclenché par la touche 'espace' qui transmet un signal récupéré par la classe Projectile et traité comme suit :

- le graphisme exécute une trajectoire rectiligne parallèle et de même direction qu'au parcours de l’objet joueur à l'instant du déclic ;

- le test de contact avec l’objet predateur est réalisé au cours de l'animation ;

- tant que l'état Avance de projectile est activé, il est impossible d'effectuer un nouveau déclenchement.

X-A-1 - Modification de la classe Joueur

Nous déclarons le signal de tir et une fonction pour émettre ce signal dans l'entête :

...
void Tirer();
virtual void MajMouvement();
signals:
void OrdreDeTir();
private:
...

La fonction public Tirer sera sollicitée par la transition partagée de l'état Conteneur.

Dans le fichier source l'instruction de transition est simple du fait que l'état Conteneur partage cette transition entre tous les états de déplacements, et que celle-ci est « sans cible » puisqu'elle agit en interne de l'état actif en cours de l’objet joueur :

TransEtPan *pan = new TransEtPan(this, QEvent::KeyPress, Qt::Key_Space);
conteneur->addTransition(pan);

La fonction Tirer contient seulement l'instruction d'émission du signal.

void Joueur::Tirer()
{
emit OrdreDeTir();
}

X-A-2 - La transition TransEtPan

C'est une classe de type QKeyEventTransition amorcée par la touche « espace », sa fonction onTransition contient l'appel à Tirer de la Classe Joueur pour envoyer le signal OrdreDeTir :

void TransEtPan::onTransition(QEvent *)
{
joueur->Tirer();
}

X-A-3 - Modification de la classe Scene

Nous initialisons l'objet projectile au niveau du constructeur, après joueur et predateur de façon à lui transmettre leurs pointeurs :

…
addItem(predateur);
projectile = new Projectile((QGraphicsItem*)predateur, joueur);
projectile->hide();
addItem(projectile);
...

Nous requalifions le type du pointeur predateur pour faciliter le travail de la fonction de collision.

Dans la suite du source, nous ajouterons les initialisations dans les fonctions NettoyerScene et InitialiserScene (voir le fichier source scene.cpp de l’étape 09).

X-A-4 - La classe Projectile

Elle a pour classe de base ObjetMobile et ne possède que deux états : arret et avance. Elle ressemble beaucoup à la classe Predateur à la différence que son démarrage est commandé par l'appui sur la touche « espace ».

Nous définissons en premier la connexion entre le signal valueChanged de l’objet animationdelobjet et le « slot » LeProjectileBouge de La classe Projectile. Cette connexion nous permettra de tester s'il y a une collision à chaque changement de l'animation. Ce principe diffère du système de recherche que nous avons utilisé dans la classe Predateur qui était réalisé lorsque son état DeplacePrd était activé :

connect(animationdelobjet, SIGNAL(valueChanged(QVariant)), this, SLOT(LeProjectileBouge(QVariant)) );

Nous créons les deux états possibles Arret et avance. Il est inutile de définir un état parent, car les transitions qui leur seront appliquées ne peuvent être partagées et ont obligatoirement une cible :

machine = new QStateMachine(this);
ArretPjl *arret = new ArretPjl(this, machine);
DeplacePjl *avance = new DeplacePjl(this, machine);

Instruisons la première transition du type QSignalTransition chargée de capter le signal OrdreDeTir émis par l’objet joueur si la touche « espace » est appuyée :

QSignalTransition *etpan = new QSignalTransition(pjoueur, SIGNAL(OrdreDeTir()));
arret->addTransition(etpan);
etpan->setTargetState(avance);

Le projectile étant initialisé au départ à son état arret, cette transition lui est ajoutée. Les deux dernières transitions suivantes sont ajoutées à l'état avance et sont validées, lorsque :

- le projectile atteint la limite de la fenêtre sans avoir eu de contact, dans ce cas, l'animation cesse et envoie le signal finished, le projectile passe alors à l'état arret et disparaît :

QSignalTransition *rearme = new QSignalTransition(animationdelobjet, SIGNAL(finished()) );
avance->addTransition(rearme);
rearme->setTargetState(arret);

- il y a eu contact, le « slot » LeProjectileBouge a émis le signal Choc, le projectile passe alors à l'état arret et disparaît :

QSignalTransition *dedans = new QSignalTransition(this, SIGNAL(Choc()));
avance->addTransition(dedans);
dedans->setTargetState(arret);

Nous terminons le constructeur par l'initialisation de la machine avec l'état arret : machine->setInitialState(arret);.

L'initialisation du projectile est simple, elle consiste à placer le graphisme dans une position temporaire, de le cacher et de démarrer la machine :

void Projectile::Initialiser()
{
setPos( 0, 0 );
hide();
BougeToi();
}

Nous esquiverons la fonction Tirer, sans grand intérêt pour ce chapitre et dont vous pourrez consulter le détail dans les sources.

Examinons à présent le «´slot » LeProjectileBouge engagé suite au signal valueChanged de l’objet animationdelobjet. Cette fonction teste s'il y a collision avec le graphisme pcible passé en paramètre :

void Projectile::LeProjectileBouge(const QVariant&)
{
if( TestCollision() )
{
animationdelobjet->stop();
emit Choc();
}
}

Si cela est : l'animation est stoppée et le signal Choc est émis. Ce test de collision est identique à celui de la classe Predateur, mais il est légèrement simplifié par l'emploi du type de base QGraphicsItem dans la déclaration du paramètre :

if( pcible == lstobjet.at(i) )
// au lieu de :
if( (ObjetImage*)pjoueur == (ObjetImage*)lstobjet.at(i) )

X-A-5 - État ArretPjl

Il est activé au début de la séquence, quand il y a contact entre les objets predateur et projectile, ou si une limite de la zone est atteinte. La fonction onEntry est réimplémentée  :

void ArretPjl::onEntry(QEvent *)
{
projectile->Initialiser();
}

C'est une initialisation en vue d'un nouveau tir.

X-A-6 - État DeplacePjl

Il est activé par le signal OrdreDeTir émis par l’objet joueur et lance le déplacement de l’objet projectile. La fonction onEntry est réimplémentée :

void DeplacePjl::onEntry(QEvent *)
{
projectile->Tirer();
}

L'instruction lance l'animation du projectile.

X-B - Déclencher une série d’objets – étape 10

Dans la section précédente, nous avons lancé un objet qui, tant qu'il n'est pas stoppé, nous interdit d'en lancer un autre. Cela est dû au fait que depuis le début de cette étude, nous n'avons jamais détruit les objets créés, la réinitialisation de chacun d'eux les restaurant à leur origine. Pour cette section, nous créerons plusieurs occurrences de la classe Projectile, comme nous l'avons fait dans l'un des tests avec une série d’objets predateur, mais ces ajouts seront provoqués par le joueur, et s’autodétruiront à la fin de leur animation. En quelque sorte, nous étions en coup par coup, maintenant, passons au tir en rafale.

Puisque nous ne créons plus l'objet projectile au début de la séquence, nous créons une fonction dans l’objet scene pour réaliser cette opération en cours d'animation :

Pour la déclencher nous n'utiliserons plus l'émission du signal OrdreDeTir de la classe Joueur, mais nous modifierons sa fonction Tirer pour envoyer l'ordre.

X-B-1 - Modification de la classe Joueur

Dans le fichier source l'instruction de transition est simple parce que l'état conteneur partage cette transition entre tous les états de déplacements existants qui lui sont affectés, et que celle-ci est « sans cible » puisqu'elle agit en interne dans l'état actif en cours de joueur :

TransEtPan *pan = new TransEtPan(this, QEvent::KeyPress, Qt::Key_Space);
conteneur->addTransition(pan);

Parce que chaque tir provoque la création d'un objet projectile dans la classe Scene, la fonction Tirer fera appel à sa fonction AjouterProjectile pour créer et démarrer cet objet :

void Joueur::Tirer()
{
static_cast<Scene*>(pvue->scene())->AjouterProjectile();
}

Ce seront les seules modifications pour la classe Joueur.

X-B-2 - Modification de la classe Scene

Le nombre de projectiles lancés n'est pas défini, nous devons donc créer une liste qui stockera les pointeurs des occurrences de projectiles en cours de déplacement dans la zone graphique. Dans l’entête le pointeur unique de projectile et remplacé par une liste chaînée de type QList, chargée de stocker et gérer ces pointeurs :

private:
...
Projectile *projectile;
// remplacé par
QList<Projectile*> lstprj;
...

Nous implémentons les deux membres pour créer et détruire ces projectiles . L'ajout en « public » qui sera appelé par Tirer de Joueur. La destruction en « private », puisque c'est la classe Scene qui s'en charge :

public :
...
void AjouterProjectile();
...
private :
...
void TuerProjectile();
...

La gestion des projectiles étant assignée à la classe Scene, nous implantons les deux slots qui seront connectés aux signaux émis par les objets projectile :

private slots:
void FinDeProjectile();
void ButAtteint();

Nous analysons maintenant les modifications dans le fichier source. Dans le constructeur nous retirons l'instance de création de l'objet projectile. De même au niveau du destructeur, où nous remplaçons la destruction de l'objet unique par l'appel à la procédure qui supprime tous les projectiles :

...
if (projectile) delete projectile;
remplacé par
TuerProjectile();
...

TuerProjectile est une fonction simple de boucle sur une « QList » supprimant chaque occurrence de projectile, il est donc inutile de la détailler.

Regardons plutôt du côté de la fonction AjouterProjectile :

- c'est dans cette fonction que nous créons l'objet projectile et que nous l'insérons à la liste.

void Scene::AjouterProjectile()
{
Projectile *pproj = new Projectile(predateur, joueur);
pproj->hide();
lstprj.append(pproj);

- ensuite, nous l'ajoutons à la scène avec addItem(pproj); ;

- maintenant, nous connectons les signaux de la classe Projectile, que nous verrons à la suite, aux slots de la classe Scene :

connect(pproj, SIGNAL(Boum()), this, SLOT(ButAtteint()) );
connect(pproj, SIGNAL(Limite()), this, SLOT(FinDeProjectile()) );

La première connexion s'active si le projectile atteint son but, et la deuxième s'il arrive à la limite de la zone graphique ;

- il ne nous reste plus qu'à envoyer l'ordre de démarrage pproj->Tirer();.

Le slot FinDeProjectile contient la suppression de l'occurrence de la liste des pointeurs et le « delete » de ce pointeur. À ce sujet, il est recommandé d'utiliser la fonction de Qt deletelater quand on utilise QStateMachine, car l'objet pourrait subsister suivant le statut de la boucle d'événement (voir la documentation Qt si besoin).

Pour notre apprentissage, le slot ButAtteint ne contient que l'appel à FinDeProjectile. Nous aurions pu y ajouter un appel à une animation de destruction de l'objet predateur, de même que dans la fonction FinDeProjectile.

ReInitialiserScene et NettoyerScene contiennent l'instruction supplémentaire : TuerProjectile();.

X-B-3 - Modification de la classe Projectile

Cette classe est quasi identique à la classe Predateur, puisqu'à part son déclenchement, elle est autonome dans son animation. Sa particularité est l'addition de trois signaux et d'un slot. Dans la description de ce qui suit, j'aurais pu simplifier la logique et minimiser l'utilisation des états et des transitions. Mais avec cette classe, nous pourrons approfondir les transitions entre états engendrées par les signaux et slots.

La description de l'entête n'est pas nécessaire, examinons tout de suite le constructeur où les pointeurs des objets predateur et joueur sont passés en paramètres.

Après les premières lignes de création de l'avatar et des initialisations de base comme pour les autres objets mobiles, nous avons une première connexion :

connect(animationdelobjet, SIGNAL( valueChanged(QVariant) ), this, SLOT(LeProjectileBouge(QVariant)) );

Celle-ci appelle la fonction LeProjectileBouge pendant le déplacement de l'objet, nous verrons plus loin que c'est nécessaire au test de collision. Puis nous initialisons les états :

ArretPjl *arret = new ArretPjl(this, machine);
DeplacePjl *avance = new DeplacePjl(this, machine);
AuBut *etpaf = new AuBut(this, machine);

Notons qu'un état conteneur est inutile, car il n'y a pas de partage de transitions envisageable.

La première transition est la gestion de la percussion entre projectile et predateur, si le signal contact est émis pendant l'état avance, il y a changement d'état vers etpaf qui déclenchera la procédure de destruction :

QSignalTransition *dedans = new QSignalTransition(this, SIGNAL(Contact()) );
avance->addTransition(dedans);
dedans->setTargetState(etpaf);

La deuxième transition concerne le ratage de la cible et donc l'arrivée à la limite de la zone graphique. Cette transition est provoquée par le signal finished de l'animation qui s'immobilise à une limite.

QSignalTransition *manquer = new QSignalTransition(animationdelobjet, SIGNAL(finished()) );
avance->addTransition(manquer);
manquer->setTargetState(arret);

Elle est ajoutée à l'état avance puisqu'elle ne peut se produire que dans cet état. Mais cette fois, elle vise l'état arrêt, car il n'y aura pas de destruction de la cible.

Nous terminons le constructeur par l'instruction : machine->setInitialState(avance);.

Effectivement lors de la création de l'occurrence de l'objet projectile, nous ne connaissons pas encore sa position de départ et, comme l'état initial est l’état avance, démarrer immédiatement la machine provoquerait certainement un incident. Ce démarrage aura lieu dans la fonction Tirer, ainsi que nous l'avons vu plus haut dans l'étude des modifications de la classe Scene cet appel se trouve à la fin de sa fonction AjouterProjectile.

Détaillons succinctement ce membre :

pntdep = pjoueur->pos(); — récupérer la position de joueur ;

switch(pjoueur->CodeDirection()) — suivant la direction, situer le point limite ;

setPos(pntdep); — positionner l’objet projectile ;

show(); — le faire apparaître dans la zone graphique ;

BougeToi(); — démarrer la machine et déclencher l'animation.

Le démarrage de la machine, provoqué par BougeToi, enclenche la fonction onEntry de l'état avance contenant l'instruction de déclenchement MajMouvement qui est la fonction virtuelle d'origine de la classe ObjetMobile. Nous aurions pu tout aussi bien placer cette instruction dans Tirer et ne pas avoir d'implémentation de onEntry dans l’état avance : le résultat aurait été identique.

Contrairement à la classe Predateur où le test de collision est réalisé dans un état, ici il est effectué dans la classe elle-même uniquement pour des raisons d'apprentissage, car ce n'est pas la meilleure solution.

Au cours de son déplacement l’objet projectile devrait entrer en contact avec l’objet predateur, nous devons donc tester « à chaque pas » cette collision pour la détecter, ce que nous avons déterminé plus haut dans le constructeur avec l'instruction :connect(animationdelobjet,.... Le signal valueChanged est émis chaque fois que la valeur currentValue de la classe QVariantAnimation change, ce qui est notre cas puisque l’objet animationdelobjet pilote le déplacement de notre objet projectile. Le slot désigné par la connexion est LeProjectileBouge :

void Projectile::LeProjectileBouge(const QVariant&)
{
if(TestCollision())
emit Contact();
}

Nous avons déjà décrit la fonction TestCollision dans l'objet predateur qui cible joueur dans un autre chapitre. En cas de collision le signal est émis.

Sachant que le déplacement a lieu pendant l'état avance, s'il y a contact la transition dedans, qui lui est attachée activera son état cible de la classe AuBut. En entrant dans la classe, nous appelons la fonction Detruire de projectile :

void AuBut::onEntry(QEvent *)
{
projectile->Detruire();
}

Dans les faits, nous ne détruisons pas la cible, nous la réinitialisons à son départ. Et son cycle de déplacement recommence :

void Projectile::Detruire()
{
hide();Stop();
pcible->Initialiser(QPointF(0,0));
emit Boum();
}

À la sortie de cette fonction, le projectile est immobile, occulté et sa machine est arrêtée, mais l'objet existe toujours, d'où l'émission du signal Boum pour le détruire dans la classe Scene où nous avons créé une connexion qui répond au signal Boum par l'appel de la fonction ButAtteint voir plus haut « Modification de la classe Scene ».

Lorsque le projectile arrive en limite, c'est la transition manquer qui entre en jeu, sa cible est l’état arret dont onEntry renvoie à la fonction Foirer. Celle-ci lance le signal Limite pour activer la connexion définie dans l’objet scene qui relie ce signal à sa fonction FinDeProjectile.

void Projectile::Foirer()
{
hide();
Stop();
emit Limite();
}

Remarquons que pour engager la procédure d'élimination de l’objet projectile, nous aurions pu utiliser : le signal finished de l'animation, ou stopped de machine, ou son membre onExit, mais cette dernière solution n'est pas recommandée.

La programmation courante des commandes usuelles de la bibliothèque QStateMachine vous est maintenant acquise. Il existe d'autres propriétés de ces états, qui vous permettront d'élaborer une programmation plus complexe et bénéfique, telle la modélisation d'états parallèles. Je vous laisse les découvrir…

XI - 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.

XII - Remerciements

Merci à dourouc05 pour ses conseils et à Claude Leloup pour la correction.