Auf dieser Seite

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

Ein bisschen Flair hinzufügen

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

Im Hinblick 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. QML bietet eine Reihe von Methoden, um flüssige Bewegungen zu erzeugen, und in diesem Fall verwenden wir den Typ „ Behavior “, um eine „ SpringAnimation “ hinzuzufügen. In „ BoomBlock.qml “ wenden wir ein „ SpringAnimation “-Verhalten auf die Eigenschaften „ x “ und „ y “ an, sodass der Block der angegebenen Position (deren Werte über „ samegame.js “ festgelegt werden) folgt und seine Bewegung federartig animiert wird. Hier ist der Code, der zu „ BoomBlock.qml “ hinzugefügt wurde:

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 federartigen Effekt der Animation anzupassen.

Die Einstellung „ enabled: spawned “ bezieht sich auf den Wert „ spawned “, der über „ createBlock() “ in „ samegame.js “ festgelegt wird. Dadurch wird sichergestellt, dass die „ SpringAnimation “ auf dem „ x “ erst aktiviert wird, nachdem „ createBlock() “ den Block an die richtige Position gebracht hat. Andernfalls rutschen die Blöcke zu Beginn des Spiels aus der Ecke (0,0) heraus, anstatt in Reihen von oben herabzufallen. (Versuchen Sie, „ enabled: spawned “ auszukommentieren, und überzeugen Sie sich selbst.)

Animieren von Änderungen der Block-Deckkraft

Als Nächstes fügen wir eine sanfte Ausblendanimation hinzu. Dazu verwenden wir den Typ „ Behavior “, mit dem wir eine Standardanimation festlegen können, wenn sich eine Eigenschaft ändert. In diesem Fall, wenn sich die „ opacity “ eines Blocks ändert, animieren wir den Opazitätswert so, dass er allmählich ein- und ausgeblendet wird, anstatt abrupt zwischen vollständig sichtbar und unsichtbar zu wechseln. Dazu 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 die Eigenschaft „ opacity: 0 “, die bedeutet, dass der Block beim Erstellen zunächst transparent ist. Wir könnten die Deckkraft in „ samegame.js “ beim Erstellen und Löschen der Blöcke festlegen, aber stattdessen verwenden wir Zustände, da dies für die nächste Animation, die wir hinzufügen werden, nützlich ist. Zunächst fügen wir diese Zustände zum Stammtyp 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 }
    }
]

Nun werden die Blöcke automatisch eingeblendet, da wir bei der Implementierung der Blockanimationen bereits „ spawned “ auf „true“ gesetzt haben. Zum Ausblenden setzen wir „ dying “ auf „true“, anstatt die Deckkraft beim Löschen eines Blocks (in der Funktion „ floodFill() “) auf 0 zu setzen.

Partikeleffekte hinzufügen

Zum Schluss fügen wir den Blöcken beim Zerstören einen cool aussehenden Partikeleffekt hinzu. Dazu fügen wir zunächst in „ BoomBlock.qml “ einen „ ParticleSystem “ wie folgt 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, solltest du den Abschnitt „Verwendung des Partikelsystems ‚ Qt Quick ‘“ lesen. Es ist jedoch wichtig zu beachten, dass ` emitRate ` auf Null gesetzt ist, damit keine Partikel wie üblich emittiert werden. Außerdem erweitern wir den Zustand ` dying `, der durch den Aufruf der Methode ` burst() ` für den Partikeltyp einen Partikelschauer erzeugt. 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); }
    }
]

Nun ist das Spiel wunderschön animiert, wobei allen Aktionen des Spielers subtile (oder auch nicht ganz so subtile) Animationen hinzugefügt wurden. Das Endergebnis ist unten dargestellt, mit verschiedenen Bildersätzen zur Veranschaulichung grundlegender Themenanpassungen:

Die Änderung des Designs erfolgt hier einfach durch das Ersetzen der Blockbilder. Dies kann zur Laufzeit durch Ändern der Eigenschaft „ Image “ ( source ) erfolgen. Als weitere Herausforderung könnten Sie also eine Schaltfläche hinzufügen, mit der zwischen Designs mit unterschiedlichen Bildern umgeschaltet werden kann.

Führen einer Bestenliste

Eine weitere Funktion, die wir dem Spiel hinzufügen könnten, ist eine Möglichkeit, Bestleistungen zu speichern und abzurufen.

Dazu zeigen wir nach Spielende einen Dialog an, in dem der Name des Spielers abgefragt und in eine Bestenliste eingetragen wird. Dies erfordert einige Änderungen an „ Dialog.qml “. Zusätzlich zum Typ „ Text “ enthält es nun ein untergeordnetes Element vom Typ „ TextInput “ zur Erfassung von Texteingaben über die Tastatur:

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

Außerdem fügen wir eine Funktion „ showWithInput() “ hinzu. Die Texteingabe ist nur sichtbar, wenn diese Funktion anstelle von „ show() “ aufgerufen wird. Wenn der Dialog geschlossen wird, sendet er ein „ closed() “-Signal aus, und andere Typen können den vom Benutzer eingegebenen Text über die Eigenschaft „ inputText “ 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();
    }
    ...
}

Nun kann der Dialog 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 “ aussendet, rufen wir die neue Funktion „ saveHighScore() “ in „ samegame.js “ auf, die den Highscore lokal in einer SQL-Datenbank speichert und den Score, sofern möglich, auch an eine Online-Datenbank übermittelt.

Das Signal „ nameInputDialog “ wird in der Funktion „ victoryCheck() “ in „ samegame.js “ ausgelöst:

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 (Erläuterung im folgenden Abschnitt), sofern es möglich ist, die Highscores an eine Online-Datenbank zu senden.

Anschließend verwenden wir die Funktion „ Local Storage API “, um eine persistente SQL-Datenbank zu verwalten, die speziell für diese Anwendung bestimmt ist. Wir erstellen mithilfe von ` openDatabaseSync() ` eine Offline-Speicherdatenbank für die Highscores und bereiten die Daten sowie die SQL-Abfrage vor, mit der wir diese speichern möchten. Die Offline-Speicher-API nutzt SQL-Abfragen zur Datenbearbeitung und zum Abruf. Im Aufruf von ` db.transaction() ` verwenden wir drei SQL-Abfragen, um die Datenbank (falls erforderlich) zu initialisieren und anschließend Bestleistungen hinzuzufügen bzw. abzurufen. Um die zurückgegebenen Daten zu nutzen, wandeln wir sie in eine Zeichenkette um, wobei jede zurückgegebene Zeile eine eigene Zeile in der Zeichenkette bildet, und zeigen einen Dialog an, der diese Zeichenkette enthält.

Dies ist eine Möglichkeit, Highscores lokal zu speichern und anzuzeigen, aber sicherlich nicht die einzige. Eine komplexere Alternative wäre es, eine Highscore-Dialogkomponente zu erstellen und ihr die Ergebnisse zur Verarbeitung und Anzeige zu übergeben (anstatt die ` Dialog` wiederzuverwenden). Dies würde einen besser an das Thema anpassbaren Dialog ermöglichen, der die Highscores besser präsentieren könnte. Wenn Ihr QML die Benutzeroberfläche für eine C++-Anwendung darstellt, hätten Sie die Punktzahl auch an eine C++-Funktion übergeben können, um sie lokal auf verschiedene Arten zu speichern, beispielsweise in einem einfachen Format ohne SQL oder in einer anderen SQL-Datenbank.

Highscores online speichern

Sie haben gesehen, wie Sie Highscores lokal speichern können, aber es ist auch einfach, einen webbasierten Highscore-Speicher 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 auf einem Server irgendwo läuft, und dieser Server speichert sie dann und zeigt sie den Besuchern an. Du könntest auch eine XML- oder QML-Datei von demselben Server anfordern, die die Punktestände enthält und anzeigt, doch das würde den Rahmen dieses Tutorials sprengen. Das hier verwendete PHP-Skript findest du im Verzeichnis „ examples “.

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

Wenn der Spieler einen Namen eingibt, senden wir die Daten mithilfe dieses Codes 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 entspricht dem in „ XMLHttpRequest() “, wie man ihn aus Standard-Browser-JavaScript kennt, und kann auf dieselbe Weise verwendet werden, um XML oder QML dynamisch vom Webdienst abzurufen und die Highscores anzuzeigen. In diesem Fall kümmern wir uns nicht um die Antwort – wir senden lediglich die Highscore-Daten an den Webserver. Hätte der Webservice eine QML-Datei (oder eine URL zu einer QML-Datei) zurückgegeben, könntet ihr diese auf ähnliche Weise wie die Blöcke instanziieren.

Eine alternative Möglichkeit, auf webbasierte Daten zuzugreifen und diese zu übermitteln, wäre die Verwendung von QML-Typen, die speziell für diesen Zweck entwickelt wurden. Mit „ XmlListModel “ lassen sich XML-basierte Daten wie RSS ganz einfach in einer QML-Anwendung abrufen und anzeigen.

Das war’s!

In diesem Tutorial hast du gesehen, wie du eine voll funktionsfähige Anwendung in QML schreiben kannst:

Es gibt noch so viel mehr über QML zu lernen, was wir in diesem Tutorial nicht behandeln konnten. Schau dir alle Beispiele und die Dokumentation an, um zu sehen, was du alles mit QML machen kannst!

Beispielprojekt auf 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.