QML高级教程4 - 最后的润饰

添加一些风格

现在我们将进行两项操作以使游戏更加生动:对区块进行动画处理并添加高分系统。

为了期待新的区块动画,现在将“Block.qml”文件重命名为“BoomBlock.qml”。

动画化区块移动

首先,我们将对区块进行动画处理,使它们以流畅的方式移动。QML有许多用于添加流体运动的方法,在此情况下,我们将使用Behavior类型来添加SpringAnimation。在BoomBlock.qml中,我们将一个SpringAnimation行为应用于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上的SpringAnimation才被启用。否则,当游戏开始时,区块将从(0,0)的角落滑出,而不是以行的方式从顶部下落。(尝试取消注释enabled: spawned并自行查看。)

动画化区块透明度变化

接下来,我们将添加一个平滑的退出动画。为此,我们将使用一个Behavior类型,它允许我们在属性更改时指定默认动画。在此情况下,在opactiy属性变更时,我们将对Image类型的透明度属性应用Behavior,使得透明度值逐渐淡入淡出,而不是在完全可见和不可见之间迅速切换。为此,我们在BoomBlock.qml中将Behavior应用于Image类型的透明度属性

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

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

要完全理解这一点,您应该阅读 使用 Qt Quick 粒子系统,但 important to note is that 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); }
    }
]

现在游戏看起来非常动感,为玩家所有动作添加了微妙(或不那么微妙的)动画。最终结果如下,不同的图像演示了基本的主题设计

这里的主题更改很简单,就是替换了块图像。这可以在运行时通过更改 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 中的 victoryCheck() 函数中调用新的 saveHighScore() 函数,该函数将高分本地保存在 SQL 数据库中,并在可能的情况下将分数发送到在线数据库。

nameInputDialogsamegame.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.js 中的 saveHighScore() 函数

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

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

然后,我们使用 本地存储API 来维护与应用程序独特的持久化SQL数据库。我们使用 openDatabaseSync() 为高分创建离线存储数据库,并准备我们要使用以保存的数据和SQL查询。离线存储API使用了SQL查询来进行数据操作和检索,而在 db.transaction() 调用中,我们使用了三个SQL查询来初始化数据库(如果需要的话),然后添加和检索高分。要使用返回的数据,我们将它转换成一个字符串,每行代表一条返回的记录,并显示包含该字符串的对话框。

这是本地存储和显示高分的一种方法,但绝对不是唯一的方法。一个更复杂的方法是创建一个高分对话框组件,并将结果传递给它进行处理和显示(而不是重用 Dialog)。这将允许创建一个更易于主题化的对话框,可以更好地展示高分。如果你的QML是C++应用的UI,你也可以将分数传递到一个C++函数中进行本地存储,多种方式存储,包括没有SQL的简单格式或在另一个SQL数据库中。

在线存储高分

你已经看到了如何在本地存储高分,但也可以轻松地将一个支持网页的高分存储集成到你的QML应用中。我们这里做的实现非常简单:高分数据被发送到一个运行在服务器上的php脚本,该服务器然后将数据存储并展示给访客。你也可以从该服务器请求一个XML或QML文件,其中包含并展示分数,但这超出了本教程的范围。我们在 examples 目录中提供的php脚本。

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

如果玩家输入了名字,我们使用 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() 是相同的,并且可以以相同的方式用于从网页服务动态获取XML或QML以显示高分。在这种情况下,我们不关心响应 - 我们只是将高分数据发送到网络服务器。如果它返回一个QML文件(或一个指向QML文件的URL),你可以以与处理图块相同的方式来实例化它。

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

这就完成了!

通过遵循这个教程,你看到了如何用QML编写一个功能齐全的应用程序。

关于QML还有许多内容我们没有在本次教程中涵盖。请查阅所有示例和文档,了解您可以用QML做到的所有事情!

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。本文档的贡献部分包含其各自所有者的版权。提供的文档是根据由自由软件基金会发布的、版本号为1.3的GNU自由文档许可证条款许可的。Qt及相关标志是Qt公司在芬兰以及其他国家的商标。所有其他商标均为其各自所有者的财产。