QML高级教程4 - 画龙点睛#

增添一些趣味#

现在我们将进行两件事来使游戏更生动:为方块添加动画并添加高分系统。

为了期待新的方块动画,Block.qml 文件现更名为 BoomBlock.qml

动画化方块移动#

首先我们将动画化方块,以便它们以流畅的方式移动。QML有多种方法可以添加流畅的运动,在这种情况下,我们将使用 行为 类型来添加 弹簧动画 。在 BoomBlock.qml 中,我们为 xy 属性应用了一个 弹簧动画 行为,以便方块可以跟随并以弹簧似的模式动画化其移动到指定的位置(其值将由 samegame.js 设置)。以下是添加到 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 }
}

可以更改 springdamping 值以修改动画的弹簧效果。

enabled: spawned 设置指的是从 samegame.js 中的 createBlock() 中设置的 spawned 值。这确保了在 createBlock() 将方块设置到正确的位置后,只有在 x 上的 弹簧动画 才被启用。否则,方块将滑动出左上角(0,0),而不是以行的方式从顶部落下。(尝试取消注释 enabled: spawned 并亲自看看。)

动画化方块不透明度变化#

接下来,我们将添加一个平滑的退出动画。为此,我们将使用 行为 类型,它允许我们在属性更改时指定默认动画。在这种情况下,当 opacity 的值为 Block 改变时,我们将动画不透明度值,以便它逐渐淡入淡出,而不是在完全可见和不可见之间突然改变。为此,我们将在 BoomBlock.qml 中的 Image 类型的 opacity 属性上应用一个 行为

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

请注意 opacity: 0,这意味着当块首次创建时是透明的。我们可以在 samegame.js 中设置透明度,在创建和销毁块时,但我们将使用 states,因为它对于我们将要添加的下一个动画很有用。最初,我们将这些状态添加到 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 }
    }
]

现在,块将自动淡入,因为我们已经在实现块动画时将 spawned 设置为 true。要淡出,当块被销毁时(在 floodFill() 函数中),我们将 dying 设置为 true 而不是将透明度设置为 0。

添加粒子效果#

最后,当块被销毁时,我们将为它们添加一个酷炫的粒子效果。要做到这一点,我们首先在 BoomBlock.qml 中添加一个 ParticleSystem,如下所示

ParticleSystem {
    id: sys
    anchors.centerIn: parent
    ImageParticle {
        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
    }

    Emitter {
        id: particles
        anchors.centerIn: parent
        emitRate: 0
        lifeSpan: 700
        velocity: AngleDirection {angleVariation: 360; magnitude: 80; magnitudeVariation: 40}
        size: 16
    }
}

要充分理解这一点,你应该阅读 使用 Qt Quick 粒子系统,但重要的是要注意 emitRate 被设置为零,这样就不会正常发出粒子。此外,我们扩展了 dying 状态,通过调用粒子的 burst() 方法来创建粒子爆发。现在状态代码如下

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

现在游戏得到了美丽的动画,为玩家的所有动作添加了细微(或不那么细微)的动画。最终结果如下所示,使用不同的一组图片来展示基本的主题设计

../_images/declarative-adv-tutorial4.gif

这里的主题更改很简单,只需替换块图片即可完成。这可以在运行时通过更改 Imagesource 属性来实现,因此作为一个额外的挑战,你可以添加一个按钮,在具有不同图片的主题之间切换。

保持高分表#

我们可能还想要为游戏添加一种存储和检索高分的机制。

为此,当游戏结束时,我们将显示一个对话框,请求玩家的名字并将其添加到高分表中。这需要对 Dialog.qml 进行一些修改。除了一个 Text 类型外,它现在有一个 TextInput 子项,用于接收键盘文本输入

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

}

我们还会添加一个 showWithInput() 函数。只有在调用此函数而不仅仅是 show() 时,文本输入才会可见。当对话框关闭时,它发出一个 closed() 信号,其他类型可以通过 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();
}            ...

}

现在这个对话框可以用于 samegame.qml

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

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

当对话框发出 closed 信号时,我们将调用 samegame.js 中的新 saveHighScore() 函数,它将高分数本地存储在 SQL 数据库中,如果可能,还会将分数发送到在线数据库。

nameInputDialog 组件在 samegame.js 文件中 victoryCheck() 函数中被激活。

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

离线存储高分

现在我们需要实现保存高分表的函数功能。

以下是 samegame.jssaveHighScore() 函数的示例

function saveHighScore(name) {
    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);
    });
}

首先,如果有可能将高分发送到在线数据库中,我们将调用 sendHighScore() 函数(在下面的章节中解释)。

然后,我们使用 Local Storage API 来维护一个针对本应用唯一的持久SQLite数据库。我们使用 openDatabaseSync() 创建一个用于高分存储的离线存储数据库,并准备好要使用的数据和SQL查询。离线存储API使用SQL查询进行数据操纵和检索,我们在 db.transaction() 调用中使用三个SQL查询来初始化数据库(如果需要),然后添加和检索高分。为了使用返回的数据,我们将它转换成一个以每行返回一个的字符串,并显示包含该字符串的对话框。

这是本地存储和显示高分的一种方法,但当然不是唯一的方法。更复杂的替代方法是创建一个高分对话框组件,并将处理和显示结果传递给它(而不是重复使用 Dialog)。这将允许更具主题性的对话框,能够更好地展示高分。如果你的QML是C++应用程序的UI,你也可以将分数传递给一个C++函数,以多种方式在本地存储它,包括无SQL的简单格式或在其他SQL数据库中。

在线存储高分

你已经看到了如何本地存储高分,但也很容易将具有Web功能的分数存储集成到您的QML应用程序中。我们在这里实现的实现非常简单:将高分数据发送到一个远程服务器上运行的PHP脚本中,然后该服务器将其存储并显示给访客。你也可以从那个服务器请求一个XML或QML文件,其中包含和显示分数,但这超出了本教程的范畴。我们使用的PHP脚本位于 examples 目录中。

如果玩家输入了他们的名字,我们可以将数据发送到我们的Web服务

如果玩家输入了名字,我们使用 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);
}

此代码中的XMLHttpRequest与标准浏览器JavaScript中的XMLHttpRequest()相同,并可以以相同的方式用于从Web服务动态获取XML或QML以显示高分。在这个情况下,我们不担心响应 - 我们只是将高分数据发送到Web服务器。如果它返回了一个QML文件(或指向QML文件的URL),你就可以像处理块一样实例化它。

访问和提交基于Web的数据的另一种方式是使用为该目的设计的QML类型。XmlListModel在QML应用程序中检索和显示基于XML的数据(如RSS)变得非常容易。

完成了!#

通过本教程,您已经了解到如何使用QML编写一个功能齐全的应用程序

  • 使用QML类型构建您的应用程序

  • 使用JavaScript代码添加应用程序逻辑

  • 使用行为状态添加动画

  • 使用,例如,QtQuick.LocalStorage或XMLHttpRequest来存储持久的应用程序数据

关于QML还有很多内容值得学习,我们在这篇教程中无法全部涵盖。请查看所有示例和 expectedResult

示例项目 @ code.qt.io