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
设置指的是samegame.js
中从createBlock()
设置的spawned
值。这样可以确保只有在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。为了淡出,我们将dying
设置为 true,而不是在块被销毁时将不透明度设置为 0(在floodFill()
函数中)。
添加粒子效果
最后,我们将在区块销毁时为其添加炫酷的粒子效果。为此,我们首先要在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: "); } }
离线存储高分
现在,我们需要实现实际保存高分表的功能。
下面是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 数据库。
在线存储高分
您已经看到了如何在本地存储高分,但将网络高分存储集成到 QML 应用程序中也很容易。我们所做的实现非常简单:高分数据被发布到某个服务器上运行的 php 脚本,然后服务器将其存储并显示给访问者。您也可以从同一服务器请求一个 XML 或 QML 文件,其中包含并显示分数,但这超出了本教程的范围。我们在此使用的 php 脚本可在examples
目录中找到。
如果玩家输入了姓名,我们就可以将数据发送到网络服务。
如果玩家输入了姓名,我们就可以使用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),则可以用与区块相同的方式将其实例化。
访问和提交基于网络的数据的另一种方法是使用为此目的设计的 QML 类型。XmlListModel 使在 QML 应用程序中获取和显示基于 XML 的数据(如 RSS)变得非常容易。
就是这样!
通过本教程,您已经了解了如何用 QML 编写功能齐全的应用程序:
- 使用以下工具构建应用程序QML types
- 用 JavaScript 代码添加应用逻辑
- 使用Behaviors 和状态添加动画
- 使用QtQuick.LocalStorage 或其他工具存储持久应用程序数据。XMLHttpRequest
关于 QML,还有很多内容是本教程无法涵盖的。请查看所有示例和文档,了解 QML 的所有功能!
© 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.