Tutoriel avancé sur QML 4 - Les dernières retouches
Ajouter une touche d'originalité
Nous allons maintenant effectuer deux opérations pour dynamiser le jeu : animer les blocs et ajouter un système de meilleurs scores.
En prévision des nouvelles animations des blocs, le fichier « Block.qml » est désormais renommé « BoomBlock.qml ».
Animation du mouvement des blocs
Nous allons tout d’abord animer les blocs afin qu’ils se déplacent de manière fluide. QML dispose de plusieurs méthodes pour ajouter des mouvements fluides, et dans ce cas précis, nous allons utiliser le type « Behavior » pour ajouter un « SpringAnimation ». Dans ` BoomBlock.qml`, nous appliquons un comportement « SpringAnimation » aux propriétés ` x ` et ` y ` afin que le bloc suive et anime son mouvement à la manière d’un ressort vers la position spécifiée (dont les valeurs seront définies par ` samegame.js`). Voici le code ajouté à ` BoomBlock.qml` :
property bool spawned: false
Behavior on x {
enabled: block.spawned;
SpringAnimation{ spring: 2; damping: 0.2 }
}
Behavior on y {
SpringAnimation{ spring: 2; damping: 0.2 }
}Les valeurs spring et damping peuvent être modifiées pour ajuster l’effet de ressort de l’animation.
Le paramètre enabled: spawned fait référence à la valeur spawned définie à partir de createBlock() dans samegame.js. Cela garantit que la fonction SpringAnimation de la classe x n’est activée qu’une fois que createBlock() a placé le bloc à la bonne position. Sinon, les blocs glisseraient hors du coin (0,0) au début d’une partie, au lieu de tomber du haut par rangées. (Essayez de mettre en commentaire enabled: spawned et constatez-le par vous-même.)
Animation des changements d’opacité des blocs
Ensuite, nous allons ajouter une animation de sortie fluide. Pour cela, nous utiliserons un type « Behavior », qui nous permet de spécifier une animation par défaut lorsqu’un changement de propriété se produit. Dans ce cas, lorsque l’ opacity d’un bloc change, nous allons animer la valeur d’opacité afin qu’elle apparaisse et disparaisse progressivement, au lieu de passer brusquement de l’état entièrement visible à l’état invisible. Pour ce faire, nous allons appliquer une Behavior à la propriété opacity du type Image dans BoomBlock.qml:
Image {
id: img
anchors.fill: parent
source: {
if (block.type == 0)
return "pics/redStone.png";
else if (block.type == 1)
return "pics/blueStone.png";
else
return "pics/greenStone.png";
}
opacity: 0
Behavior on opacity {
NumberAnimation { properties:"opacity"; duration: 200 }
}
}Notez l’ opacity: 0, qui signifie que le bloc est transparent dès sa création. Nous pourrions définir l’opacité dans samegame.js lors de la création et de la destruction des blocs, mais nous allons plutôt utiliser des états, car cela s’avère utile pour la prochaine animation que nous allons ajouter. Dans un premier temps, nous ajoutons ces états au type racine de BoomBlock.qml:
property bool dying: false
states: [
State{ name: "AliveState"; when: spawned == true && dying == false
PropertyChanges { target: img; opacity: 1 }
},
State{ name: "DeathState"; when: dying == true
PropertyChanges { target: img; opacity: 0 }
}
]Désormais, les blocs apparaîtront automatiquement en fondu, puisque nous avons déjà défini spawned sur true lors de la mise en œuvre des animations des blocs. Pour les faire disparaître en fondu, nous définissons dying sur true au lieu de définir l’opacité sur 0 lorsqu’un bloc est détruit (dans la fonction floodFill() ).
Ajout d’effets de particules
Enfin, nous allons ajouter un effet de particules sympa aux blocs lorsqu’ils sont détruits. Pour ce faire, nous ajoutons d’abord une ParticleSystem dans BoomBlock.qml, comme suit :
ParticleSystem {
id: sys
anchors.centerIn: parent
ImageParticle {
// ![0]
source: {
if (block.type == 0)
return "pics/redStar.png";
else if (block.type == 1)
return "pics/blueStar.png";
else
return "pics/greenStar.png";
}
rotationVelocityVariation: 360
// ![0]
}
Emitter {
id: particles
anchors.centerIn: parent
emitRate: 0
lifeSpan: 700
velocity: AngleDirection {angleVariation: 360; magnitude: 80; magnitudeVariation: 40}
size: 16
}
}Pour bien comprendre ce principe, nous vous recommandons de consulter la section « Utilisation du système de particules Qt Quick », mais il est important de noter que emitRate est défini sur zéro afin que les particules ne soient pas émises normalement. De plus, nous étendons l’état dying, ce qui crée une rafale de particules en appelant la méthode burst() sur le type de particules. Le code des états se présente désormais comme suit :
states: [
State {
name: "AliveState"
when: block.spawned == true && block.dying == false
PropertyChanges { img.opacity: 1 }
},
State {
name: "DeathState"
when: block.dying == true
StateChangeScript { script: particles.burst(50); }
PropertyChanges { img.opacity: 0 }
StateChangeScript { script: block.destroy(1000); }
}
]Le jeu bénéficie désormais de superbes animations, avec des effets subtils (ou moins subtils) ajoutés pour toutes les actions du joueur. Le résultat final est présenté ci-dessous, avec une série d’images différentes pour illustrer la personnalisation de base :
Le changement de thème est ici obtenu simplement en remplaçant les images des blocs. Cela peut être effectué à l’exécution en modifiant la propriété ` Image ` de ` source `. Pour relever un défi supplémentaire, vous pourriez donc ajouter un bouton permettant de basculer entre des thèmes comportant des images différentes.
Tenir un tableau des meilleurs scores
Une autre fonctionnalité que nous pourrions ajouter au jeu est un moyen de stocker et de récupérer les meilleurs scores.
Pour ce faire, nous afficherons une boîte de dialogue à la fin de la partie afin de demander le nom du joueur et de l’ajouter à un tableau des meilleurs scores. Cela nécessite quelques modifications dans ` Dialog.qml`. En plus d’un élément de type ` Text `, il comporte désormais un élément enfant de type ` TextInput ` destiné à recevoir la saisie de texte au clavier :
Rectangle {
id: container
...
TextInput {
id: textInput
anchors { verticalCenter: parent.verticalCenter; left: dialogText.right }
width: 80
text: ""
onAccepted: container.hide() // close dialog when Enter is pressed
}
...
}Nous allons également ajouter une fonction showWithInput(). La zone de saisie ne sera visible que si cette fonction est appelée à la place de show(). Lorsque la boîte de dialogue est fermée, elle émet un signal closed(), et les autres types peuvent récupérer le texte saisi par l’utilisateur via la propriété inputText:
Rectangle {
id: container
property string inputText: textInput.text
signal closed
function show(text) {
dialogText.text = text;
container.opacity = 1;
textInput.opacity = 0;
}
function showWithInput(text) {
show(text);
textInput.opacity = 1;
textInput.focus = true;
textInput.text = ""
}
function hide() {
textInput.focus = false;
container.opacity = 0;
container.closed();
}
...
}La boîte de dialogue peut désormais être utilisée dans ` samegame.qml` :
Dialog {
id: nameInputDialog
anchors.centerIn: parent
z: 100
onClosed: {
if (nameInputDialog.inputText != "")
SameGame.saveHighScore(nameInputDialog.inputText);
}
}Lorsque la boîte de dialogue émet le signal « closed », nous appelons la nouvelle fonction « saveHighScore() » dans « samegame.js », qui stocke le meilleur score localement dans une base de données SQL et envoie également ce score à une base de données en ligne si possible.
L'nameInputDialog est activée dans la fonction victoryCheck() de samegame.js:
function victoryCheck() {
...
//Check whether game has finished
if (deservesBonus || !(floodMoveCheck(0, maxRow - 1, -1))) {
gameDuration = new Date() - gameDuration;
nameInputDialog.showWithInput("You won! Please enter your name: ");
}
}Enregistrement des meilleurs scores hors ligne
Nous devons maintenant implémenter la fonctionnalité permettant d’enregistrer la table des meilleurs scores.
Voici la fonction ` saveHighScore() ` dans ` samegame.js` :
function saveHighScore(name) {
if (gameCanvas.score == 0)
return;
if (scoresURL != "")
sendHighScore(name);
var db = Sql.LocalStorage.openDatabaseSync("SameGameScores", "1.0", "Local SameGame High Scores", 100);
var dataStr = "INSERT INTO Scores VALUES(?, ?, ?, ?)";
var data = [name, gameCanvas.score, maxColumn + "x" + maxRow, Math.floor(gameDuration / 1000)];
db.transaction(function(tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(name TEXT, score NUMBER, gridSize TEXT, time NUMBER)');
tx.executeSql(dataStr, data);
var rs = tx.executeSql('SELECT * FROM Scores WHERE gridSize = "12x17" ORDER BY score desc LIMIT 10');
var r = "\nHIGH SCORES for a standard sized grid\n\n"
for (var i = 0; i < rs.rows.length; i++) {
r += (i + 1) + ". " + rs.rows.item(i).name + ' got ' + rs.rows.item(i).score + ' points in ' + rs.rows.item(i).time + ' seconds.\n';
}
dialog.show(r);
});
}Nous appelons d’abord la fonction ` sendHighScore() ` (expliquée dans la section ci-dessous) s’il est possible d’envoyer les meilleurs scores vers une base de données en ligne.
Ensuite, nous utilisons la fonction Local Storage API pour gérer une base de données SQL persistante propre à cette application. Nous créons une base de données de stockage hors ligne pour les meilleurs scores à l’aide de openDatabaseSync() et préparons les données ainsi que la requête SQL que nous souhaitons utiliser pour les enregistrer. L’API de stockage hors ligne utilise des requêtes SQL pour la manipulation et la récupération des données ; dans l’appel ` db.transaction() `, nous utilisons trois requêtes SQL pour initialiser la base de données (si nécessaire), puis pour y ajouter et récupérer les meilleurs scores. Pour exploiter les données renvoyées, nous les convertissons en une chaîne de caractères contenant une ligne par ligne renvoyée, puis affichons une boîte de dialogue contenant cette chaîne.
Il s’agit là d’une façon de stocker et d’afficher les meilleurs scores localement, mais ce n’est certainement pas la seule. Une alternative plus complexe consisterait à créer un composant de boîte de dialogue dédié aux meilleurs scores, et à lui transmettre les résultats pour traitement et affichage (au lieu de réutiliser l’ Dialog). Cela permettrait d’obtenir une boîte de dialogue plus personnalisable, capable de mieux présenter les meilleurs scores. Si votre code QML constitue l’interface utilisateur d’une application C++, vous auriez également pu transmettre le score à une fonction C++ afin de le stocker localement de différentes manières, notamment dans un format simple sans SQL ou dans une autre base de données SQL.
Stockage des meilleurs scores en ligne
Vous avez vu comment stocker les meilleurs scores localement, mais il est également facile d’intégrer un système de stockage des meilleurs scores via le Web dans votre application QML. L’implémentation que nous avons réalisée ici est très simple : les données des meilleurs scores sont envoyées à un script PHP s’exécutant sur un serveur quelque part, et ce serveur les stocke puis les affiche aux visiteurs. Vous pourriez également demander à ce même serveur un fichier XML ou QML contenant et affichant les scores, mais cela dépasse le cadre de ce tutoriel. Le script PHP que nous utilisons ici est disponible dans le répertoire examples.
Si le joueur a saisi son nom, nous pouvons envoyer ces données au service web
Si le joueur saisit un nom, nous envoyons les données au service à l'aide de ce code situé dans samegame.js:
function sendHighScore(name) {
var postman = new XMLHttpRequest()
var postData = "name=" + name + "&score=" + gameCanvas.score + "&gridSize=" + maxColumn + "x" + maxRow + "&time=" + Math.floor(gameDuration / 1000);
postman.open("POST", scoresURL, true);
postman.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
postman.onreadystatechange = function() {
if (postman.readyState == postman.DONE) {
dialog.show("Your score has been uploaded.");
}
}
postman.send(postData);
}La requête XMLHttpRequest de ce code est identique à celle de XMLHttpRequest(), comme vous le trouverez dans le JavaScript standard des navigateurs, et peut être utilisée de la même manière pour récupérer dynamiquement du XML ou du QML depuis le service web afin d’afficher les meilleurs scores. Dans ce cas précis, nous ne nous soucions pas de la réponse : nous nous contentons d’envoyer les données des meilleurs scores au serveur web. S’il avait renvoyé un fichier QML (ou une URL vers un fichier QML), vous auriez pu l’instancier de la même manière que vous l’avez fait avec les blocs.
Une autre façon d’accéder à des données Web et de les envoyer consiste à utiliser des types QML conçus à cet effet. XmlListModel facilite grandement la récupération et l’affichage de données XML, telles que les flux RSS, dans une application QML.
Et voilà !
En suivant ce tutoriel, vous avez découvert comment écrire une application entièrement fonctionnelle en QML :
- Compilez votre application avec QML types
- Ajoutez la logique métier avec du code JavaScript
- Ajoutez des animations à l’aide d’ Behaviors et des états
- Enregistrez les données persistantes de l'application à l'aide, par exemple, d'QtQuick.LocalStorage ou de XMLHttpRequest
Il y a encore tant à apprendre sur QML que nous n'avons pas pu aborder dans ce tutoriel. Consultez tous les exemples et la documentation pour découvrir tout ce que vous pouvez faire avec QML !
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.