QML 高级教程 4 - 收尾工作
增添一些趣味
现在,我们将通过两项操作来让游戏更加生动:为方块添加动画效果,并加入高分系统。
为了配合新的方块动画,Block.qml 文件现已重命名为BoomBlock.qml 。
为方块移动添加动画
首先,我们将为方块添加动画效果,使其移动更加流畅。QML 提供了多种实现流畅运动的方法,在此我们使用Behavior 类型来添加SpringAnimation 效果。 在BoomBlock.qml 中,我们将SpringAnimation 行为应用于x 和y 属性,使方块以弹簧般的运动方式跟随并动画化地移动至指定位置(该位置的值将由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 }
}可以通过更改spring 和damping 的值来调整动画的弹簧般效果。
enabled: spawned 设置所指的spawned 值,是由samegame.js 中的createBlock() 设定的。这确保了只有在createBlock() 将方块定位到正确位置后,x 上的SpringAnimation 才会被启用。否则,游戏开始时方块会从 (0,0) 角滑出,而不是按行从顶部落下。(试着将enabled: spawned 注释掉,亲自验证一下。)
方块不透明度变化的动画效果
接下来,我们将添加一个平滑的退出动画。为此,我们将使用Behavior 类型,它允许我们在属性发生变化时指定默认动画。 在此情况下,当块的opacity 发生变化时,我们将对不透明度值进行动画处理,使其逐渐淡入淡出,而不是在完全可见和完全不可见之间突然切换。为此,我们将在BoomBlock.qml 中,对Image 类型的opacity 属性应用Behavior :
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 中创建和销毁方块时设置不透明度,但我们将改用状态,因为这对接下来要添加的动画非常有用。首先,我们将这些状态添加到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 粒子系统》,但需要注意的是,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); }
}
]现在,游戏拥有了精美的动画效果,玩家的所有操作都配有细腻(或不那么细腻)的动画效果。最终效果如下所示,其中使用了一组不同的图片来演示基础主题效果:
此处的主题切换仅通过替换方块图像即可实现。这可以通过在运行时修改 `Image ` 的 `source ` 属性来完成,因此作为进一步的挑战,你可以添加一个按钮,用于在带有不同图像的主题之间切换。
维护高分榜
我们可能还想为游戏添加另一项功能:存储和检索高分的方法。
为此,我们将在游戏结束时显示一个对话框,请求玩家输入姓名并将其添加到高分榜中。这需要对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: ");
}
}离线存储高分
现在我们需要实现实际保存“High Scores”表的功能。
以下是 中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() (相关内容将在下文说明)。
然后,我们使用 `Local Storage API ` 来维护一个专属于此应用程序的持久化 SQL 数据库。我们通过 `openDatabaseSync() ` 为高分创建一个离线存储数据库,并准备好用于保存数据所需的数据和 SQL 查询。 离线存储 API 使用 SQL 查询进行数据操作和检索;在调用db.transaction() 时,我们使用三个 SQL 查询来初始化数据库(如有必要),然后添加和检索最高分。为了使用返回的数据,我们将数据转换为字符串(每行返回一个字符串),并显示一个包含该字符串的对话框。
这是一种在本地存储和显示高分的方案,但绝非唯一的方式。一种更复杂的替代方案是创建一个高分对话框组件,并将结果传递给它进行处理和显示(而不是重复使用Dialog )。这样可以实现更具主题可定制性的对话框,从而更好地展示高分。 如果您的 QML 是 C++ 应用程序的用户界面,您还可以将分数传递给一个 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 types
- 使用 JavaScript 代码添加应用程序逻辑
- 使用Behaviors 和状态添加动画
- 使用QtQuick.LocalStorage 或XMLHttpRequest等方法存储持久化应用程序数据
关于 QML 还有许多内容尚未在本教程中涵盖。请查阅所有示例和文档,了解 QML 的全部功能!
© 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.