Sur cette page

Tutoriel QML avancé 4 - Touches finales

Ajouter un peu d'ambiance

Nous allons maintenant faire deux choses pour égayer le jeu : animer les blocs et ajouter un système de score élevé.

En prévision des nouvelles animations de blocs, le fichier Block.qml a été renommé BoomBlock.qml.

Animation du mouvement des blocs

Tout d'abord, nous allons animer les blocs afin qu'ils se déplacent de manière fluide. QML dispose d'un certain nombre de méthodes pour ajouter un mouvement fluide et, dans ce cas, nous allons utiliser le type Behavior pour ajouter une propriété 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 de spring et damping peuvent être modifiées pour modifier l'effet de ressort de l'animation.

Le paramètre enabled: spawned fait référence à la valeur spawned qui est définie à partir de createBlock() dans samegame.js. Cela permet de s'assurer que l'action de SpringAnimation sur x n'est activée qu'après que createBlock() a placé le bloc dans la bonne position. Sinon, les blocs glisseront hors du coin (0,0) au début du jeu, au lieu de tomber du haut en rangées. (Essayez de commenter enabled: spawned et vous verrez par vous-même).

Animation des changements d'opacité des blocs

Ensuite, nous allons ajouter une animation de sortie en douceur. 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'adresse opacity d'un bloc change, nous animerons la valeur d'opacité de manière à ce qu'elle s'estompe progressivement, au lieu de passer brusquement d'une visibilité totale à une invisibilité totale. Pour ce faire, nous appliquerons un Behavior sur 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 que opacity: 0 signifie que le bloc est transparent lors de sa création. Nous pourrions définir l'opacité dans samegame.js lorsque nous créons et détruisons les blocs, mais nous allons plutôt utiliser des états, car cela est 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 }
    }
]

Maintenant, les blocs vont automatiquement s'effacer, car nous avons déjà fixé spawned à true lorsque nous avons implémenté les animations des blocs. Pour le fondu sortant, nous fixons dying à true au lieu de fixer l'opacité à 0 lorsqu'un bloc est détruit (dans la fonction floodFill() ).

Ajout d'effets de particules

Enfin, nous allons ajouter un effet de particules aux blocs lorsqu'ils sont détruits. Pour ce faire, nous ajoutons d'abord un 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, vous devriez lire Using the Qt Quick Particle System, mais il est important de noter que emitRate est mis à zéro pour que les particules ne soient pas émises normalement. Nous étendons également l'état dying, qui crée une rafale de particules en appelant la méthode burst() sur le type particules. Le code des états ressemble maintenant à ceci :

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 est maintenant magnifiquement animé, avec des animations subtiles (ou moins subtiles) ajoutées pour toutes les actions du joueur. Le résultat final est illustré ci-dessous, avec une série d'images différentes pour démontrer le thème de base :

Le changement de thème s'effectue simplement en remplaçant les blocs d'images. Cela peut se faire au moment de l'exécution en modifiant la propriété Image source . Pour un défi supplémentaire, vous pouvez ajouter un bouton qui bascule entre les thèmes avec des images différentes.

Tenue d'un tableau des meilleurs scores

Une autre fonctionnalité que nous pourrions vouloir ajouter au jeu est une méthode de stockage et de récupération des meilleurs scores.

Pour ce faire, nous afficherons une boîte de dialogue à la fin du jeu pour demander le nom du joueur et l'ajouter à un tableau des meilleurs scores. Pour ce faire, nous devons apporter quelques modifications à Dialog.qml. En plus d'un type Text, il possède désormais un élément enfant TextInput pour recevoir les entrées de texte du 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 ajouterons également une fonction showWithInput(). La saisie de texte ne sera visible que si cette fonction est appelée au lieu de show(). Lorsque la boîte de dialogue est fermée, elle émet un signal closed(), et d'autres types peuvent récupérer le texte saisi par l'utilisateur grâce à une 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();
    }
    ...
}

Le dialogue peut maintenant être utilisé 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 le score à une base de données en ligne si possible.

La fonction 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: ");
    }
}
Stockage des meilleurs scores hors ligne

Nous devons maintenant implémenter la fonctionnalité permettant de sauvegarder le tableau 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);
    });
}

Tout d'abord, nous appelons sendHighScore() (expliqué dans la section ci-dessous) s'il est possible d'envoyer les meilleurs scores à une base de données en ligne.

Ensuite, nous utilisons Local Storage API pour maintenir 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 et la requête SQL que nous voulons utiliser pour les sauvegarder. L'API de stockage hors ligne utilise des requêtes SQL pour la manipulation et la récupération des données et, dans l'appel db.transaction(), nous utilisons trois requêtes SQL pour initialiser la base de données (si nécessaire), puis pour ajouter et récupérer les meilleurs scores. Pour utiliser les données renvoyées, nous les transformons en une chaîne de caractères avec une ligne par ligne renvoyée, et nous affichons une boîte de dialogue contenant cette chaîne de caractères.

Il s'agit 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 dialogue sur les scores élevés et à lui transmettre les résultats pour qu'il les traite et les affiche (au lieu de réutiliser le site Dialog). Cela permettrait de créer une boîte de dialogue plus thématique qui présenterait mieux les meilleurs scores. Si votre QML est l'interface utilisateur d'une application C++, vous auriez également pu transmettre le score à une fonction C++ pour le stocker localement de différentes manières, y compris dans un format simple sans SQL ou dans une autre base de données SQL.

Stockage des scores élevés en ligne

Vous avez vu comment vous pouvez stocker les scores élevés localement, mais il est également facile d'intégrer un stockage des scores élevés sur le Web dans votre application QML. L'implémentation que nous avons faite ici est très simple : les données relatives aux scores élevés sont envoyées à un script php fonctionnant sur un serveur quelque part, et ce serveur les stocke et les affiche aux visiteurs. Vous pourriez également demander à ce même serveur un fichier XML ou QML qui contiendrait et afficherait 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 les données au service web que nous utilisons.

Si le joueur entre un nom, nous envoyons les données au service en utilisant ce code 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);
}

Le XMLHttpRequest dans ce code est le même que le XMLHttpRequest() que vous trouverez dans le JavaScript standard du navigateur, et peut être utilisé de la même manière pour obtenir dynamiquement du XML ou du QML du service web pour afficher les meilleurs scores. Dans ce cas, nous ne nous préoccupons pas de la réponse - nous envoyons simplement les données relatives aux meilleurs scores au serveur web. S'il avait renvoyé un fichier QML (ou une URL vers un fichier QML), vous pourriez 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 basées sur le web et de les soumettre serait d'utiliser les types QML conçus à cet effet. XmlListModel permet de récupérer et d'afficher très facilement des données basées sur le XML, telles que les RSS, dans une application QML.

Voilà, c'est fini !

En suivant ce tutoriel, vous avez vu comment vous pouvez écrire une application entièrement fonctionnelle en QML :

Il y a beaucoup plus à apprendre sur QML que nous n'avons pas pu couvrir dans ce tutoriel. Consultez tous les exemples et la documentation pour voir tout ce que vous pouvez faire avec QML !

Exemple de projet @ code.qt.io

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