QML-Tutorial für Fortgeschrittene 4 - Der letzte Schliff

Etwas Flair hinzufügen

Jetzt werden wir zwei Dinge tun, um das Spiel zu beleben: die Blöcke animieren und ein Highscore-System hinzufügen.

Im Vorgriff auf die neuen Blockanimationen wird die Datei Block.qml nun in BoomBlock.qml umbenannt.

Animation der Blockbewegung

Zunächst werden wir die Blöcke so animieren, dass sie sich flüssig bewegen. BoomBlock.qml QML verfügt über eine Reihe von Methoden zum Hinzufügen von fließenden Bewegungen, und in diesem Fall werden wir den Typ Behavior verwenden, um eine SpringAnimation hinzuzufügen. In BoomBlock.qml wenden wir ein SpringAnimation -Verhalten auf die Eigenschaften x und y an, so dass der Block seiner Bewegung folgt und auf eine federähnliche Weise in Richtung der angegebenen Position (deren Werte durch samegame.js festgelegt werden) animiert wird:

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

Die Werte spring und damping können geändert werden, um den federähnlichen Effekt der Animation zu modifizieren.

Die Einstellung enabled: spawned bezieht sich auf den Wert spawned, der von createBlock() in samegame.js eingestellt wird. Dadurch wird sichergestellt, dass SpringAnimation auf x erst dann aktiviert wird, wenn createBlock() den Block in die richtige Position gebracht hat. Andernfalls rutschen die Blöcke aus der Ecke (0,0) heraus, wenn ein Spiel beginnt, anstatt reihenweise von oben herabzufallen. (Versuchen Sie, enabled: spawned auszukommentieren und sehen Sie selbst).

Animierte Änderungen der Block-Deckkraft

Als Nächstes werden wir eine sanfte Ausstiegsanimation hinzufügen. Dazu verwenden wir den Typ Behavior, mit dem wir eine Standardanimation für die Änderung einer Eigenschaft festlegen können. In diesem Fall, wenn sich die opacity eines Blocks ändert, werden wir den Deckkraftwert so animieren, dass er allmählich ein- und ausgeblendet wird, anstatt abrupt zwischen vollständig sichtbar und unsichtbar zu wechseln. Zu diesem Zweck wenden wir eine Behavior auf die Eigenschaft opacity des Typs Image in BoomBlock.qml an:

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

Beachten Sie das opacity: 0, das bedeutet, dass der Block bei seiner ersten Erstellung transparent ist. Wir könnten die Deckkraft in samegame.js einstellen, wenn wir die Blöcke erstellen und zerstören, aber stattdessen werden wir Zustände verwenden, da dies für die nächste Animation, die wir hinzufügen werden, nützlich ist. Zunächst fügen wir diese Zustände dem Root-Typ von BoomBlock.qml hinzu:

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 }
    }
]

Jetzt werden die Blöcke automatisch eingeblendet, da wir spawned bereits auf true gesetzt haben, als wir die Blockanimationen implementiert haben. Um die Blöcke auszublenden, setzen wir dying auf true, anstatt die Opazität auf 0 zu setzen, wenn ein Block zerstört wird (in der Funktion floodFill() ).

Hinzufügen von Partikeleffekten

Schließlich fügen wir den Blöcken einen cool aussehenden Partikeleffekt hinzu, wenn sie zerstört werden. Dazu fügen wir zunächst eine ParticleSystem in BoomBlock.qml hinzu:

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

Um dies vollständig zu verstehen, sollten Sie das Qt Quick Partikelsystem verwenden lesen, aber es ist wichtig zu beachten, dass emitRate auf Null gesetzt ist, damit die Partikel nicht normal emittiert werden. Außerdem erweitern wir den Zustand dying, der einen Partikelstoß erzeugt, indem wir die Methode burst() für den Partikeltyp aufrufen. Der Code für die Zustände sieht nun wie folgt aus:

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); }
    }
]

Jetzt ist das Spiel wunderschön animiert, mit subtilen (oder auch nicht so subtilen) Animationen für alle Aktionen des Spielers. Das Endergebnis ist unten zu sehen, mit einer anderen Reihe von Bildern, um das grundlegende Theming zu demonstrieren:

Der Themenwechsel wird hier einfach durch das Ersetzen der Blockbilder erzeugt. Dies kann zur Laufzeit durch Ändern der Eigenschaft Image source erfolgen. Als weitere Herausforderung können Sie eine Schaltfläche hinzufügen, mit der Sie zwischen den Themen mit verschiedenen Bildern wechseln können.

Führen einer Tabelle mit hohen Punktzahlen

Eine weitere Funktion, die wir dem Spiel vielleicht hinzufügen möchten, ist eine Methode zum Speichern und Abrufen von Highscores.

Dazu werden wir am Ende des Spiels einen Dialog einblenden, der den Namen des Spielers abfragt und ihn in eine High-Scores-Tabelle einträgt. Dazu sind einige Änderungen an Dialog.qml erforderlich. Zusätzlich zum Typ Text gibt es jetzt ein untergeordnetes Element TextInput für den Empfang von Tastatureingaben:

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

Wir werden auch eine showWithInput() Funktion hinzufügen. Die Texteingabe wird nur sichtbar, wenn diese Funktion anstelle von show() aufgerufen wird. Wenn der Dialog geschlossen wird, gibt er ein closed() Signal aus, und andere Typen können den vom Benutzer eingegebenen Text über eine inputText Eigenschaft abrufen:

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();
    }
    ...
}

Jetzt kann das Dialogfeld in samegame.qml verwendet werden:

Dialog {
    id: nameInputDialog
    anchors.centerIn: parent
    z: 100

    onClosed: {
        if (nameInputDialog.inputText != "")
            SameGame.saveHighScore(nameInputDialog.inputText);
    }
}

Wenn der Dialog das Signal closed ausgibt, rufen wir die neue Funktion saveHighScore() in samegame.js auf, die den Highscore lokal in einer SQL-Datenbank speichert und den Score nach Möglichkeit auch an eine Online-Datenbank sendet.

Die nameInputDialog wird in der Funktion victoryCheck() in samegame.js aktiviert:

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: ");
    }
}
Highscores offline speichern

Nun müssen wir die Funktionalität implementieren, um die High Scores Tabelle tatsächlich zu speichern.

Hier ist die Funktion saveHighScore() in 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);
    });
}

Zunächst rufen wir sendHighScore() auf (siehe unten), wenn es möglich ist, die Highscores an eine Online-Datenbank zu senden.

Dann verwenden wir Local Storage API, um eine persistente SQL-Datenbank zu pflegen, die nur für diese Anwendung gilt. Wir erstellen eine Offline-Speicherdatenbank für die Highscores mit openDatabaseSync() und bereiten die Daten und die SQL-Abfrage vor, die wir zum Speichern verwenden wollen. Die Offline-Speicher-API verwendet SQL-Abfragen zur Datenbearbeitung und -abfrage, und im Aufruf von db.transaction() verwenden wir drei SQL-Abfragen, um die Datenbank zu initialisieren (falls erforderlich) und dann Highscores hinzuzufügen und abzurufen. Um die zurückgegebenen Daten zu verwenden, wandeln wir sie in eine Zeichenkette mit einer Zeile pro zurückgegebener Zeile um und zeigen ein Dialogfeld mit dieser Zeichenkette an.

Dies ist eine Möglichkeit, Highscores lokal zu speichern und anzuzeigen, aber sicher nicht die einzige. Eine komplexere Alternative wäre, eine Highscore-Dialogkomponente zu erstellen und ihr die Ergebnisse zur Verarbeitung und Anzeige zu übergeben (anstatt die Dialog wiederzuverwenden). Dies würde einen besser gestaltbaren Dialog ermöglichen, der die Highscores besser darstellen könnte. Wenn Ihr QML die Benutzeroberfläche für eine C++-Anwendung ist, könnten Sie den Punktestand auch an eine C++-Funktion übergeben, um ihn lokal auf verschiedene Arten zu speichern, einschließlich eines einfachen Formats ohne SQL oder in einer anderen SQL-Datenbank.

Online-Speicherung von Highscores

Sie haben gesehen, wie Sie Highscores lokal speichern können, aber es ist auch einfach, eine webfähige Highscore-Speicherung in Ihre QML-Anwendung zu integrieren. Die Implementierung, die wir hier vorgenommen haben, ist sehr einfach: Die Highscore-Daten werden an ein PHP-Skript gesendet, das irgendwo auf einem Server läuft, und dieser Server speichert sie dann und zeigt sie den Besuchern an. Sie könnten auch eine XML- oder QML-Datei vom selben Server anfordern, die die Punktzahlen enthält und anzeigt, aber das würde den Rahmen dieses Tutorials sprengen. Das php-Skript, das wir hier verwenden, ist im Verzeichnis examples verfügbar.

Wenn der Spieler seinen Namen eingegeben hat, können wir die Daten an den Webservice senden

Wenn der Spieler einen Namen eingibt, senden wir die Daten mit diesem Code in samegame.js an den Dienst:

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);
}

Der XMLHttpRequest in diesem Code ist derselbe wie der XMLHttpRequest(), den Sie in Standard-Browser-JavaScript finden, und kann auf dieselbe Weise verwendet werden, um dynamisch XML oder QML vom Webdienst zu erhalten, um die Highscores anzuzeigen. Wir kümmern uns in diesem Fall nicht um die Antwort - wir senden nur die Highscore-Daten an den Webserver. Wenn der Dienst eine QML-Datei (oder eine URL zu einer QML-Datei) zurückgegeben hätte, könnten Sie ihn genauso instanziieren, wie Sie es mit den Blöcken getan haben.

Eine andere Möglichkeit, auf webbasierte Daten zuzugreifen und sie zu übermitteln, wäre die Verwendung von QML-Typen, die für diesen Zweck entwickelt wurden. XmlListModel macht es sehr einfach, XML-basierte Daten wie RSS in einer QML-Anwendung abzurufen und anzuzeigen.

Das war's!

In diesem Lernprogramm haben Sie gesehen, wie Sie eine voll funktionsfähige Anwendung in QML schreiben können:

Es gibt noch so viel mehr über QML zu lernen, das wir in diesem Lernprogramm nicht abdecken konnten. Schauen Sie sich alle Beispiele und die Dokumentation an, um zu sehen, was Sie alles mit QML machen können!

Beispielprojekt @ code.qt.io

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