×
Namespaces

Variants
Actions
Revision as of 17:44, 25 January 2011 by rdrincon (Talk | contribs)

A QML Memory Game Tutorial

From Nokia Developer Wiki
Jump to: navigation, search

Underconstruction.pngUnder Construction: This article is under construction and it may have outstanding issues. If you have any comments please use the comments tab.

In this article we will cover very basic QML elements that will help you to create an easy but cool Memory game using the recent QML technology.

Contents

About The Game

What Is the Game About

This is the game description in short

  • This game consists of a deck with 32 paired cards arranged randomly.
  • You open a card by clicking or tapping on it, if so, the card will show its content
  • You can open a second card in the same way
  • If cards match, then they are removed from the deck
  • If cards don't match, they cover themselves again
  • Match or not, the pair counter will go up 1 unit and this is displayed in the top/right
  • On the top/left you will be able to see the elapsed time


Here is an image of the game scene:


QMLMemory.jpg


Main Menu and other Screens

The game contains a main menu presented in two different scenarios

  • When the app starts or game is over: two options (Start/Exit)
  • When the game is paused: three options (Continue/Restart/Exit)

It also contains an End Dialog displayed when all the cards are matched and the game is over.


QMLMemoryMenus.jpg


Constructing The Game

This is the list of concepts we need to keep in mind for making this game. QML objects are a set of Items that contain properties (defined by the Item or by the user), states, transitions and many other things we will not cover here. Items can be defined by the language like Rectangle, Image, Text or can be just a container (in this case only use Item). An Item can contain as many other items as it is necessary and these enclosed Items can have other Items, etc.


Creating a single Card

Basically, the card is composed by two Images, one is the cover and the other one is the content. The cover is a single Image and we can address it directly, but the content could be any of the 16 Images of the deck and must be passed from outside. To do this, we can use properties and bind the content Image to this property.


We can rotate the images in its default state using a transform. In this case we will use a Rotation With this, we can rotate the image in any of its axes.


The idea is to have one image perpendicular to the screen, meaning rotated 90 degrees on the Y axis and then not visible; and the other one parallel to the screen, rotated 0 degrees on the Y axis and then visible. These rotations need an id, which is a defined property we can use to address it later and change one of its properties.


These is for instance the code of the image that contains the content of the card

//Content of the card
Image {
id: contentImg
 
source: "img/card"+parNumber+".PNG"
 
// Centers the image on its own container
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
 
//this rotation produces de effect of the card content turning over and showing up
transform: Rotation{
id: contentRotation
origin.x: 35;
origin.y: 35;
axis { x: 0; y: 1; z: 0 }
angle: 90
}
}

The way an Item can communicate to the outside world is using signals . This is very simple, you only have to declare the signal and emit it in some time you need (usually when the user taps/clicks on some specific Item).

    signal selected
 
MouseArea {
 
id: interactiveArea
anchors.fill: parent //expand the MouseArea to be contained in its parent Item
onClicked: card.selected()
}

On the outside world you can then use the following property to address the signal directly

onSelected:  <some javascript code>



We also need to understand the concept of States and Transitions.


  • State: Is a set of values of different properties of an Item in a very specific instant of time.

For example, think about a car (Item), it has an engine (property) and different states like off and on. In on state, the engine is turned on, in off state, the engine is off.

  • Transitions: Is something that happens when you change from one state to another. You can animate the change of value of one property between states using a set of animations defined in QML.


Following these ideas, our card will basically have 3 different states


  • closed: The cover of the card is shown. This is the default state and its represented by an empty quotes ""
  • Open: The content of the card is shown
  • removed: The card is not visible and has no interaction


We can create a very cool transition between Closed and Open states using a the rotation on the Y axis in each of the two images.


This is how we create the effect: On a closed state we rotate the content Image over the Y axis 90 degrees, it is there, but you cannot see it. The cover Image is then shown completely.

On an Open state, we first rotate the cover Image 90 degrees so it is not visible (perpendicular to the screen), after that is finished, we rotate the content Image 90 degrees (parallel to the screen) so it is completely visible. We have achieved a flip animation. The only parameter to change in this case is the angle of the rotation, and the sequence will be very important since it has to come one rotation after the other, we can do that in this way (Remember "" is our closed or default state):

        Transition {
from: ""
to: "open"
reversible: true
//This animation produces the effect of the card flipping over and showing the content
SequentialAnimation{
NumberAnimation { target: coverRotation; property: "angle"; duration: 150 }
NumberAnimation { target: contentRotation; property: "angle"; duration: 150 }
}
}

You will see as well in the code bellow a transition between the "Open" and "removed" states that creates a spin effect rotating the card and decreasing its size.


This is the complete Card code:

//  Card.qml
import QtQuick 1.0
 
 
//This item represents a single card on the deck
Item {
 
id: card
width:75
height:75
 
signal selected
property string parNumber: '0'
 
//Cover of the card
Image {
id: coverImg
source: "img/cover.PNG"
 
// Centers the image on its own container
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
 
//This rotation produces the effect of the card cover turning over
transform: Rotation{
id: coverRotation
origin.x: 35;
origin.y: 35;
axis { x: 0; y: 1; z: 0 }
angle: 0
}
 
}
 
//Content of the card
Image {
id: contentImg
 
source: "img/card"+parNumber+".PNG"
 
// Centers the image on its own container
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
 
//this rotation produces de effect of the card content turning over and showing up
transform: Rotation{
id: contentRotation
origin.x: 35;
origin.y: 35;
axis { x: 0; y: 1; z: 0 }
angle: 90
}
}
 
MouseArea {
 
id: interactiveArea
anchors.fill: parent
onClicked: card.selected()
}
 
 
//-----------STATES---------------------
 
states: [
//State presented when the card is flipped over
State {
name: "open"
PropertyChanges {
target: coverRotation
angle: 90
}
PropertyChanges {
target: contentRotation
angle: 0
 
}
},
//State presented when the card is removed from the deck
State {
name: "removed"
PropertyChanges {
target: contentRotation
angle:0
 
}
PropertyChanges {
target: coverImg
visible: false
}
 
PropertyChanges {
target: interactiveArea
enabled: false
 
PropertyChanges {
target: contentImg
width:0
height:0
rotation:360
}
}
]
 
 
// ------------- TRANSITIONS-------------------------
 
transitions: [
Transition {
from: ""
to: "open"
reversible: true
//This annimation produces the effect of the card flipping over and showing the content
SequentialAnimation{
NumberAnimation { target: coverRotation; property: "angle"; duration: 150 }
NumberAnimation { target: contentRotation; property: "angle"; duration: 150 }
}
},
Transition {
from: "open"
to: "removed"
NumberAnimation { target: contentImg; properties: "rotation,width,height"; duration: 200 }
 
}
]
}

Creating the deck and game logic

Now we need to construct the place where the game is held and its logic. At the end of this tutorial is the complete source code if you happen to get lost somewhere.

Deck

For the deck we will use a positioner, this is a kind of Item that allows you to arrange elements in a particular way. The positioner we will use is a Grid, you need to specify the number of rows and columns that will make this Grid. Inside a positioner you can use a Repeater, this element is the most similar thing in QML to a bucle. The model determines the number of iterations and you have access to the index property with the number of the current iteration.

The deck will be then created like this:

    //Deck holding the 32 cards
Grid{
id: deck
rows: 4
columns: 8
 
//Repeater will let us arrange the 32 cards easily
Repeater{
model: 32
 
//When the selected signal is emited, the openCard method is called
//and the index is passed as argument
Card{onSelected: openCard(index)}
}
}

Game Logic

The logic of the game is completely done in JavaScript. We can always mix QML code with JavaScript code.

The first thing to do is to arrange the cards randomly on the deck. In the following code, remember that we use the property of the card parNumber to get the content image since we have an image folder with 16 different images called like this card1.PNG, card2.PNG, etc

    //Randomize the cards on the deck
function randomizeCards(){
 
var sortedArray = ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']
 
var date = new Date()
var mils = date.getMilliseconds() //Use miliseconds avoids the same random secuece generation among calls
 
for(var i=0;i<32;i++){
var located = false
while(!located){
var randomnumber = Math.floor((Math.random()*mils)%32) // we retrieve the integer part of the generated number no higher than 31
 
var content= sortedArray[randomnumber]
if(content !=''){ //if field is already empty, try again
deck.children[i].parNumber = content // If a number has been found, asign it to the following Card
sortedArray[randomnumber]=''
located=true; //go for the next iteration
}
}
}
 
}


Now we have a set of 32 random cards (16 pairs) on the table. We will use two several properties to keep track of the game, appart from some index holders we will have a property to keep the number of the pairs open (matched or not) and another to keep the number of the remaining couples to be found so we know when the game is over.

The logic will be pretty simple:

  1. card1 is open, index is stored
  2. card 2 is open, index is stored
  3. a Timer of 1 second is launched
  4. when the Timer is triggered a function to check if cards are equal is called
  5. If cards are equal, cards are removed
  6. If cards are different, cards are closed again


Timer is actually a QML item that can be started or stopped at any time

    //When two cards are open, this timer is enabled to close them in 1 second
Timer{
id:closeTimer
interval: 1000
onTriggered: validatePar()
}


And the code to implement this functionality is the following

     //custom properties to hold values
property int card1 //holds the index of the first card selected
property int card2 //holds the index of the second card selected
property int parCount //indicates how many pairs have been open in total (doesnt mean they necesary match)
property int remaining //Indicates how many couples are left to end the game
 
//This function is triggered when a card is selected
function openCard(index){
 
//Changes the state of the card to open
deck.children[index].state = "open"
 
//Storages the index of the card in one of the two placeholders card1 or card2
if(card1==-1){
card1=index
}
else{
 
if(index== card1) return //return if its the same card selected
 
card2=index
deck.enabled = false //disables the deck while the pair is closed
closeTimer.start() //closeTimer is enabled and will close the cards in one second
}
}
 
 
//Validate if the two open cards match or not, called when the closeTimer is triggered
function validatePar(){
 
var parNumber1 = deck.children[card1].parNumber
var parNumber2 = deck.children[card2].parNumber
var state = ""
 
//If cards are equal they are removed
if(parNumber1==parNumber2){
state = "removed"
remaining-- //one less card to find
}
 
deck.children[card1].state = state
deck.children[card2].state = state
 
//restablish initial values
card1 = -1
card2 = -1
deck.enabled = true
parCount++
 
 
//If no remaining pairs, end of the game
if(remaining==0){
game.state="finished"
}
}


Keeping track of the time

We need to keep track of the elapsed time of the game so we can later create some "Best times" functionality (not included in this tutorial), or just to be aware of the time it takes for one person to complete the game. This requires some Items to display the information and some logic to calculate the time. We will keep the track of miliseconds in a variable. We use miliseconds because it is easier to construct a Date object with it to format properly We will have a Timer item that triggers a callback every second and in this callback we implement some logic to format the elapsed time and update the Items that display the information on the screen.


//--- QML code ---
//This timer is triggered every second to keep the timer on the top-left running
Timer{
id:elapsedTimer
interval: 1000
running: false
onTriggered: calculateElapsedTime()
repeat: true
 
//--- Javascript code ---
 
 
//This function shows the timer on the upper-left side of the screen
function calculateElapsedTime(){
 
miliseconds+=1000 //Since we use a timer with 1000 mseconds we add this time to the counter
 
var date = new Date(miliseconds)
 
//Arrange the format
 
var seg = date.getUTCSeconds()
seg = seg >9 ? seg : '0'+seg
 
var mins = date.getUTCMinutes()
mins = mins>9 ? mins : '0'+mins
 
//Display the elapsed time
elapsedTimeText.text = mins+":"+seg
 
}
 
}
362 page views in the last 30 days.