![]() |
Home · All Classes · All Functions · Overviews |
[Previous: QML Advanced Tutorial 3 - Implementing the Game Logic] [QML Advanced Tutorial]
Now we're going to do two things to liven the game up. Animate the blocks and add a web-based high score system.
If you compare the samegame3 directory with samegame4, you'll noticed that we've cleaned the directory structure up. We now have a lot of files, and so they've been split up into folders - the most notable one being a content folder which we've placed all the QML but the main file.
The most vital animations are that the blocks move fluidly around the board. QML has many tools for fluid behavior, and in this case we're going to use the SpringFollow element. By having the script set targetX and targetY, instead of x and y directly, we can set the x and y of the block to a follow. SpringFollow is a property value source, which means that you can set a property to be one of these elements and it will automatically bind the property to the element's value. The SpringFollow's value follows another value over time, when the value it is tracking changes the SpringFollow's value will also change, but it will move smoothly there over time with a spring-like movement (based on the spring parameters specified). This is shown in the below snippet of code from Block.qml:
property int targetX: 0 property int targetY: 0 x: SpringFollow { source: targetX; spring: 2; damping: 0.2 } y: SpringFollow { source: targetY; spring: 2; damping: 0.2 }
We also have to change the samegame.js code, so that wherever it was setting the x or y it now sets targetX and targetY (including when creating the block). This simple change is all you need to get spring moving blocks that no longer teleport around the board. If you try doing just this though, you'll notice that they now never jump from one point to another, even in the initialization! This gives an odd effect of having them all slide out of the corner (0,0) on start up. We'd rather that they fall down from the top in rows. To do this, we disable the x follow (but not the y follow) and only enable it after we've set the x in the createBlock function. The above snippet now becomes:
property bool spawned: false property int targetX: 0 property int targetY: 0 x: SpringFollow { enabled: spawned; source: targetX; spring: 2; damping: 0.2 } y: SpringFollow { source: targetY; spring: 2; damping: 0.2 }
The next-most vital animation is a smooth exit. For this animation, we'll use a Behavior element. A Behavior is also a property value source, and it is much like SpringFollow except that it doesn't model the behavior of a spring. You specify how a Behavior transitions using the standard animations. As we want the blocks to smoothly fade in and out we'll set a Behavior on the block image's opacity, like so:
Image { id: img source: { if(type == 0){ "pics/redStone.png"; } else if(type == 1) { "pics/blueStone.png"; } else { "pics/greenStone.png"; } } opacity: 0 opacity: Behavior { NumberAnimation { properties:"opacity"; duration: 200 } } anchors.fill: parent }
Note that the opacity: 0 makes it start out transparent. We could set the opacity in the script file when we create and destroy the blocks, but instead we use states (as this is useful for the next animation we'll implement). The below snippet is set on the root element of Block.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 } } ]
Now it will automatically fade in, as we set spawned to true already when implementing the block movement animations. To fade out, we set 'dying' to true instead of setting opacity to 0 when a block is destroyed (in the floodFill function).
The least vital animations are a cool-looking particle effect when they get destroyed. First we create a Particles element in the block, like so:
Particles { id: particles width:1; height:1; anchors.centerIn: parent; emissionRate: 0; lifeSpan: 700; lifeSpanDeviation: 600; angle: 0; angleDeviation: 360; velocity: 100; velocityDeviation:30; source: { if(type == 0){ "pics/redStar.png"; } else if (type == 1) { "pics/blueStar.png"; } else { "pics/greenStar.png"; } } }
To fully understand this you'll want to look at the Particles element documentation, but it's important to note that emissionRate is set to zero, so that no particles are emitted normally. We next extend the 'dying' state, which creates a burst of particles by calling the burst method on the particles element. The code for the states now look like this:
states: [ State{ name: "AliveState"; when: spawned == true && dying == false PropertyChanges { target: img; opacity: 1 } }, State{ name: "DeathState"; when: dying == true StateChangeScript { script: particles.burst(50); } PropertyChanges { target: img; opacity: 0 } StateChangeScript { script: block.destroy(1000); } } ]
And now the game should be beautifully animated and smooth, with a subtle (or not-so-subtle) animation added for all of the player's actions. The end result is shown below, with a different set of images to demonstrate basic themeing:
The basic theme change there is the result of simply replacing the images. This can be done at run time by setting the source property, so a further advanced feature to try on your own is to add a button which toggles between two different themes.
Another extension we might want for the game is some way of storing and retrieving high scores. This tutorial contains both online and offline high score storage.
For better high score data, we want the name and time of the player. The time is obtained in the script fairly simply, but we have to ask the player for their name. We thus re-use the dialog QML file to pop up a dialog asking for the player's name (and if they exit this dialog without entering it they have a way to opt out of posting their high score). When the dialog is closed we store the name and high score, using the code below.
function saveHighScore(name) { if(scoresURL!="") sendHighScore(name); //OfflineStorage var db = openDatabaseSync("SameGameScores", "1.0", "Local SameGame High Scores",100); var dataStr = "INSERT INTO Scores VALUES(?, ?, ?, ?)"; var data = [name, gameCanvas.score, maxX+"x"+maxY ,Math.floor(timer/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); } ); }
For offline storage, we use the HTML 5 offline storage JavaScript API to maintain a persistant SQL database unique to this application. This first line in this function calls the function for the web-based high scores, described later, if it has been setup. Next we create an offline storage database for the high scores using openDatabase and prepare the data and SQL query that we want to use to save it. The offline storage API uses SQL queries for data manipulation and retrival, and in the db.transaction call we use three SQL queries to initialize the database (if necessary), and then add to and retrieve high scores. To use the returned data, we turn it into a string with one line per row returned, and show a dialog containing that string. For a more detailed explanation of the offline storage API in QML, consult the global object documentation.
This is one way of storing and displaying high scores locally, but not the only way. A more complex alternative would have been to create a high score dialog component, and pass the results to it for processing and display (instead of resusing the Dialog). This would allow a more themable dialog that could present the high scores better. If your QML is the UI for a C++ application, you could also have passed the score to a C++ function to store it locally in a variety of ways, including a simple format without SQL or in another SQL database.
You've seen how to store high scores locally, but it is also easy to integrate a web enabled high score storage into your QML application. This tutorial also shows you how to communicate the high scores to a web server. The implementation we've done is very simple - the high score data is posted to a php script running on a server somewhere, and that server then stores it and displays it to visitors. You could request an XML or QML file from that same server, which contained and displayed the scores, but that's beyond the scope of this tutorial. The php script we've used is available in the examples directory.
if the player entered their name we can send the data to the web service in the following snippet out of the script file:
function sendHighScore(name) { var postman = new XMLHttpRequest() var postData = "name="+name+"&score="+gameCanvas.score +"&gridSize="+maxX+"x"+maxY +"&time="+Math.floor(timer/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); }
This is the same XMLHttpRequest() as you'll find in browser JavaScript, and can be used in the same way to dynamically get XML or QML from the web service to display the high scores. We don't worry about the response in this case, we just post the high score data to the web server. If it had returned a QML file (or a URL to a QML file) you could instantiate it in much the same way as you did the blocks.
An alternate way to access and submit web-based data would be to use QML elements designed for this purpose - XmlListModel makes it very easy to fetch and display XML based data such as RSS in a QML application (see the Flickr demo for an example).
By following this tutorial you've now ben shown how to write a fully functional application in QML, with the application logic written in a script file and with both many fluid animations and being web-enabled. Congratulations, you should now be skilled enough to write entire applications in QML.
[Previous: QML Advanced Tutorial 3 - Implementing the Game Logic] [QML Advanced Tutorial]
Copyright © 2010 Nokia Corporation and/or its subsidiary(-ies) | Trademarks | Qt 4.7.0 |