×
Namespaces

Variants
Actions
(Difference between revisions)

A QML Memory Game Tutorial

From Nokia Developer Wiki
Jump to: navigation, search
hamishwillee (Talk | contribs)
m (Hamishwillee - Bot update)
hamishwillee (Talk | contribs)
m (Hamishwillee - Undo revision 133643 by Hamishwillee (talk) - failed bot update)
Line 1: Line 1:
{{ArticleMetaData <!-- v1. 2 -->
+
[[Category:Qt]][[Category:Qt Quick]][[Category:Code Examples]][[Category:Code Snippet]][[Category:Code Examples]][[Category:Code Snippet]]
|sourcecode= [[Media:QMLMemoryFiles. zip]]  
+
{{FeaturedArticle}}This article shows how to implement a simple memory game using Qt Quick.
|installfile= <!-- Link to installation file (e. g. [[Media:The Installation File. sis]]) -->
+
|devices= <!-- Devices tested against - e. g. ''devices=Nokia 6131 NFC, Nokia C7-00'') -->
+
|sdk= <!-- SDK(s) built and tested against (e. g. [http://linktosdkdownload/ Qt SDK 1. 1. 4]) -->
+
|platform= <!-- Compatible platforms - e. g. Symbian^1 and later, Qt 4. 6 and later -->
+
|devicecompatability= <!-- Compatible devices e. g.: All* (must have internal GPS) -->
+
|dependencies= <!-- Any other/external dependencies e. g.: Google Maps Api v1. 0 -->
+
|signing= <!-- Signing requirements - empty or one of: Self-Signed, DevCert, Manufacturer -->
+
|capabilities= <!-- Capabilities required by the article/code example (e. g. Location, NetworkServices. -->
+
|keywords= <!-- APIs, classes and methods (e. g. QSystemScreenSaver, QList, CBase -->
+
|language= <!-- Language category code for non-English topics - e. g. Lang-Chinese -->
+
|translated-by= <!-- [[User:XXXX]] -->
+
|translated-from-title= <!-- Title only -->
+
|translated-from-id= <!-- Id of translated revision -->
+
|review-by= <!-- After re-review: [[User:username]] -->
+
|review-timestamp= <!-- After re-review: YYYYMMDD -->
+
|update-by= <!-- After significant update: [[User:username]]-->
+
|update-timestamp= <!-- After significant update: YYYYMMDD -->
+
|creationdate= 20110125
+
|author= [[User:Rdrincon]]
+
}}
+
[[Category:Qt]][[Category:Qt Quick]]
+
{{FeaturedArticle|timestamp=20110130}}This article shows how to implement a simple memory game using Qt Quick.
+
 
{{Abstract|visible=false|This article shows how to create a simple memory game using Qt Quick. In this game users attempt to pair all the matching cards from a deck (which has been placed face-down) in the shortest possible time. The article explains the code in detail, from how to create one card, through to creating the entire deck and implementing the game logic.}}
 
{{Abstract|visible=false|This article shows how to create a simple memory game using Qt Quick. In this game users attempt to pair all the matching cards from a deck (which has been placed face-down) in the shortest possible time. The article explains the code in detail, from how to create one card, through to creating the entire deck and implementing the game logic.}}
  
Line 42: Line 20:
  
  
[[File:QMLMemory. jpg]]
+
[[File:QMLMemory.jpg]]
  
  
Line 55: Line 33:
  
  
[[File:QMLMemoryMenus. jpg]]
+
[[File:QMLMemoryMenus.jpg]]
  
  
Line 92: Line 70:
  
 
         // Centers the image on its own container
 
         // Centers the image on its own container
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
  
 
         //this rotation produces de effect of the card content turning over and showing up
 
         //this rotation produces de effect of the card content turning over and showing up
 
         transform:  Rotation{
 
         transform:  Rotation{
 
             id: contentRotation
 
             id: contentRotation
             origin. x: 35;
+
             origin.x: 35;
             origin. y: 35;
+
             origin.y: 35;
 
             axis { x: 0; y: 1; z: 0 }
 
             axis { x: 0; y: 1; z: 0 }
 
             angle: 90
 
             angle: 90
Line 117: Line 95:
  
 
         id: interactiveArea
 
         id: interactiveArea
         anchors. fill: parent    //expand the MouseArea to be contained in its parent Item
+
         anchors.fill: parent    //expand the MouseArea to be contained in its parent Item
         onClicked: card. selected()
+
         onClicked: card.selected()
 
     }
 
     }
  
Line 138: Line 116:
  
 
* '''State''':  Is a set of values of different properties of an Item in a very specific instant of time.
 
* '''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.
+
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.
+
* '''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.
  
  
Line 146: Line 124:
  
  
* '''closed''':  The cover of the card is shown. This is the default state and its represented by an empty quotes ""
+
* '''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
 
* '''Open''':  The content of the card is shown
 
* '''removed''':  The card is not visible and has no interaction
 
* '''removed''':  The card is not visible and has no interaction
Line 158: Line 136:
 
The cover Image is then shown completely.
 
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.   
+
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):
+
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):
  
 
<code javascript>  
 
<code javascript>  
Line 182: Line 160:
 
<code javascript>  
 
<code javascript>  
  
//  Card. qml
+
//  Card.qml
import QtQuick 1. 0
+
import QtQuick 1.0
  
  
Line 199: Line 177:
 
     Image {
 
     Image {
 
         id: coverImg
 
         id: coverImg
         source: "img/cover. PNG"
+
         source: "img/cover.PNG"
 
          
 
          
 
         // Centers the image on its own container
 
         // Centers the image on its own container
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
  
 
         //This rotation produces the effect of the card cover turning over
 
         //This rotation produces the effect of the card cover turning over
 
         transform:  Rotation{
 
         transform:  Rotation{
 
             id: coverRotation
 
             id: coverRotation
             origin. x: 35;
+
             origin.x: 35;
             origin. y: 35;
+
             origin.y: 35;
 
             axis { x: 0; y: 1; z: 0 }
 
             axis { x: 0; y: 1; z: 0 }
 
             angle: 0
 
             angle: 0
Line 223: Line 201:
  
 
         // Centers the image on its own container
 
         // Centers the image on its own container
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
  
 
         //this rotation produces de effect of the card content turning over and showing up
 
         //this rotation produces de effect of the card content turning over and showing up
 
         transform:  Rotation{
 
         transform:  Rotation{
 
             id: contentRotation
 
             id: contentRotation
             origin. x: 35;
+
             origin.x: 35;
             origin. y: 35;
+
             origin.y: 35;
 
             axis { x: 0; y: 1; z: 0 }
 
             axis { x: 0; y: 1; z: 0 }
 
             angle: 90
 
             angle: 90
Line 239: Line 217:
  
 
         id: interactiveArea
 
         id: interactiveArea
         anchors. fill: parent
+
         anchors.fill: parent
         onClicked: card. selected()
+
         onClicked: card.selected()
 
     }
 
     }
  
Line 313: Line 291:
 
== Creating the deck and game logic ==
 
== 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.
+
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 ===
 
=== Deck ===
Line 319: Line 297:
 
For the deck we will use a '''positioner''', this is a kind of Item that allows you to arrange elements in a particular way.
 
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.
 
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.   
+
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:
 
The deck will be then created like this:
Line 348: Line 326:
  
 
The first thing to do is to arrange the cards randomly on the deck.  
 
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
+
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
  
 
<code javascript>
 
<code javascript>
Line 358: Line 336:
  
 
         var date = new Date()
 
         var date = new Date()
         var mils = date. getMilliseconds()      //Use miliseconds avoids the same random secuece generation among calls
+
         var mils = date.getMilliseconds()      //Use miliseconds avoids the same random secuece generation among calls
  
 
         for(var i=0;i<32;i++){
 
         for(var i=0;i<32;i++){
 
             var located = false
 
             var located = false
 
             while(!located){
 
             while(!located){
                 var randomnumber = Math. floor((Math. random()*mils)%32)    // we retrieve the integer part of the generated number no higher than 31
+
                 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]
 
                 var content= sortedArray[randomnumber]
 
                 if(content !=''){  //if field is already empty, try again
 
                 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
+
                     deck.children[i].parNumber = content                        // If a number has been found, asign it to the following Card
 
                     sortedArray[randomnumber]=''
 
                     sortedArray[randomnumber]=''
 
                     located=true;              //go for the next iteration
 
                     located=true;              //go for the next iteration
Line 379: Line 357:
  
  
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, apart 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.
+
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, apart 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:
 
The logic will be pretty simple:
Line 419: Line 397:
  
 
         //Changes the state of the card to open
 
         //Changes the state of the card to open
         deck. children[index].state = "open"
+
         deck.children[index].state = "open"
  
 
         //Storages the index of the card in one of the two placeholders card1 or card2
 
         //Storages the index of the card in one of the two placeholders card1 or card2
Line 430: Line 408:
  
 
             card2=index
 
             card2=index
             deck. enabled = false    //disables the deck while the pair is closed
+
             deck.enabled = false    //disables the deck while the pair is closed
             closeTimer. start()      //closeTimer is enabled and will close the cards in one second
+
             closeTimer.start()      //closeTimer is enabled and will close the cards in one second
 
         }
 
         }
 
     }
 
     }
Line 439: Line 417:
 
     function validatePar(){
 
     function validatePar(){
  
         var parNumber1 = deck. children[card1].parNumber
+
         var parNumber1 = deck.children[card1].parNumber
         var parNumber2 = deck. children[card2].parNumber
+
         var parNumber2 = deck.children[card2].parNumber
 
         var state = ""
 
         var state = ""
  
Line 449: Line 427:
 
         }
 
         }
  
         deck. children[card1].state = state
+
         deck.children[card1].state = state
         deck. children[card2].state = state
+
         deck.children[card2].state = state
  
 
         //restablish initial values
 
         //restablish initial values
 
         card1 = -1
 
         card1 = -1
 
         card2 = -1
 
         card2 = -1
         deck. enabled = true
+
         deck.enabled = true
 
         parCount++
 
         parCount++
  
Line 461: Line 439:
 
         //If no remaining pairs, end of the game
 
         //If no remaining pairs, end of the game
 
         if(remaining==0){
 
         if(remaining==0){
         game. state="finished"
+
         game.state="finished"
 
         }
 
         }
 
     }
 
     }
Line 472: Line 450:
 
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.
 
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.
 
This requires some Items to display the information and some logic to calculate the time.
We will keep the track of milliseconds in a variable. We use milliseconds because it is easier to construct a Date object with it to give it a proper format.
+
We will keep the track of milliseconds in a variable. We use milliseconds because it is easier to construct a Date object with it to give it a proper format.
 
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.
 
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.
  
Line 484: Line 462:
 
     Text{
 
     Text{
 
         id: elapsedTimeText
 
         id: elapsedTimeText
         anchors. top: parent. top
+
         anchors.top: parent.top
         anchors. topMargin: 10
+
         anchors.topMargin: 10
         anchors. left: crono. right
+
         anchors.left: crono.right
         anchors. leftMargin: 5
+
         anchors.leftMargin: 5
  
 
     }
 
     }
  
   //This timer is triggered every second to keep the timer on the top-left running. It is started manually when the game begins using the start() function
+
   //This timer is triggered every second to keep the timer on the top-left running. It is started manually when the game begins using the start() function
 
     Timer{
 
     Timer{
 
         id:elapsedTimer
 
         id:elapsedTimer
Line 511: Line 489:
 
         //Arrange the format
 
         //Arrange the format
  
         var seg = date. getUTCSeconds()
+
         var seg = date.getUTCSeconds()
 
         seg = seg >9 ? seg : '0'+seg
 
         seg = seg >9 ? seg : '0'+seg
  
         var mins = date. getUTCMinutes()
+
         var mins = date.getUTCMinutes()
 
         mins = mins>9 ? mins : '0'+mins
 
         mins = mins>9 ? mins : '0'+mins
  
 
         //Display the elapsed time
 
         //Display the elapsed time
         elapsedTimeText. text = mins+":"+seg
+
         elapsedTimeText.text = mins+":"+seg
  
 
     }
 
     }
Line 532: Line 510:
 
we will create a mainMenu in a different file and will refer to it in the game file.
 
we will create a mainMenu in a different file and will refer to it in the game file.
 
The main menu is displayed when the game is stopped or paused.
 
The main menu is displayed when the game is stopped or paused.
If the game is stopped it will contain only two options:  Start and Exit. We will call this state "normal"  
+
If the game is stopped it will contain only two options:  Start and Exit. We will call this state "normal"  
If the game is paused, it will contain more options:  Continue, Restart and Exit. We will call this state "extended"
+
If the game is paused, it will contain more options:  Continue, Restart and Exit. We will call this state "extended"
 
If the game is running, the mainMenu will be hidden, we will call this a "hidden" state.
 
If the game is running, the mainMenu will be hidden, we will call this a "hidden" state.
  
Line 539: Line 517:
  
 
<code javascript>  
 
<code javascript>  
// MainMenu. qml
+
// MainMenu.qml
  
import QtQuick 1. 0
+
import QtQuick 1.0
  
  
Line 559: Line 537:
 
     transform:  Rotation{
 
     transform:  Rotation{
 
         id: contentRotation
 
         id: contentRotation
         origin. x: 160;
+
         origin.x: 160;
         origin. y: 0;
+
         origin.y: 0;
 
         axis { x: 1; y: 0; z: 0 }
 
         axis { x: 1; y: 0; z: 0 }
 
         angle: 0
 
         angle: 0
Line 571: Line 549:
 
         height: 320
 
         height: 320
 
         color: "cornflowerblue"
 
         color: "cornflowerblue"
         border. width: 3
+
         border.width: 3
         border. color: "darkblue"
+
         border.color: "darkblue"
         opacity: 0. 8
+
         opacity: 0.8
 
         radius: 10
 
         radius: 10
 
     }
 
     }
Line 583: Line 561:
 
         rows: 3
 
         rows: 3
 
         columns: 1
 
         columns: 1
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
 
         spacing: 15
 
         spacing: 15
  
Line 597: Line 575:
  
 
             color: "orange"
 
             color: "orange"
             border. width: 3
+
             border.width: 3
             border. color: "orangered"
+
             border.color: "orangered"
  
 
             visible: false
 
             visible: false
Line 606: Line 584:
 
                 id: continueText
 
                 id: continueText
 
                 text: "Continue"
 
                 text: "Continue"
                 anchors. verticalCenter: parent. verticalCenter
+
                 anchors.verticalCenter: parent.verticalCenter
                 anchors. horizontalCenter: parent. horizontalCenter
+
                 anchors.horizontalCenter: parent.horizontalCenter
                 font. bold: true
+
                 font.bold: true
                 font. pointSize: 12
+
                 font.pointSize: 12
 
                 color: "white"
 
                 color: "white"
 
             }
 
             }
  
 
             MouseArea{
 
             MouseArea{
                 anchors. fill: parent
+
                 anchors.fill: parent
                 onClicked: mainMenu. continued()    //When pressed the continued signal is emitted
+
                 onClicked: mainMenu.continued()    //When pressed the continued signal is emitted
 
             }
 
             }
 
         }
 
         }
Line 629: Line 607:
  
 
             color: "orange"
 
             color: "orange"
             border. width: 3
+
             border.width: 3
             border. color: "orangered"
+
             border.color: "orangered"
  
 
             visible: false
 
             visible: false
Line 638: Line 616:
 
                 id: startText
 
                 id: startText
 
                 text: "Start"
 
                 text: "Start"
                 anchors. verticalCenter: parent. verticalCenter
+
                 anchors.verticalCenter: parent.verticalCenter
                 anchors. horizontalCenter: parent. horizontalCenter
+
                 anchors.horizontalCenter: parent.horizontalCenter
                 font. bold: true
+
                 font.bold: true
                 font. pointSize: 12
+
                 font.pointSize: 12
 
                 color: "white"
 
                 color: "white"
 
             }
 
             }
 
             MouseArea{
 
             MouseArea{
                 anchors. fill: parent
+
                 anchors.fill: parent
                 onClicked: mainMenu. started()      //emits the start signal
+
                 onClicked: mainMenu.started()      //emits the start signal
 
             }
 
             }
 
         }
 
         }
Line 660: Line 638:
 
             radius: 10
 
             radius: 10
 
             color: "orange"
 
             color: "orange"
             border. width: 3
+
             border.width: 3
             border. color: "orangered"
+
             border.color: "orangered"
  
  
Line 667: Line 645:
 
                 id: exitText
 
                 id: exitText
 
                 text: "Exit"
 
                 text: "Exit"
                 anchors. verticalCenter: parent. verticalCenter
+
                 anchors.verticalCenter: parent.verticalCenter
                 anchors. horizontalCenter: parent. horizontalCenter
+
                 anchors.horizontalCenter: parent.horizontalCenter
                 font. bold: true
+
                 font.bold: true
                 font. pointSize: 12
+
                 font.pointSize: 12
 
                 color: "white"
 
                 color: "white"
 
             }
 
             }
  
 
             MouseArea{
 
             MouseArea{
                 anchors. fill: parent
+
                 anchors.fill: parent
                 onClicked: mainMenu. exited()  //emits the exited signal
+
                 onClicked: mainMenu.exited()  //emits the exited signal
 
             }
 
             }
 
         }
 
         }
Line 766: Line 744:
 
// End..qml
 
// End..qml
  
import QtQuick 1. 0
+
import QtQuick 1.0
  
  
Line 782: Line 760:
 
         id: background
 
         id: background
 
         color: "green"
 
         color: "green"
         opacity: 0. 8
+
         opacity: 0.8
 
         width: 320
 
         width: 320
 
         height: 320
 
         height: 320
 
         radius: 10
 
         radius: 10
         border. width: 3
+
         border.width: 3
         border. color: "darkgreen"
+
         border.color: "darkgreen"
  
 
     }
 
     }
Line 793: Line 771:
 
     //Once touched it sends a selected signal
 
     //Once touched it sends a selected signal
 
     MouseArea{
 
     MouseArea{
         anchors. fill: parent
+
         anchors.fill: parent
         onClicked: endMessage. selected()
+
         onClicked: endMessage.selected()
 
     }
 
     }
  
Line 801: Line 779:
 
         width: 180
 
         width: 180
 
         height: 180
 
         height: 180
         source: "img/welldone. PNG"
+
         source: "img/welldone.PNG"
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
  
 
     }
 
     }
Line 810: Line 788:
 
         id: name
 
         id: name
 
         text: "WELL DONE!"
 
         text: "WELL DONE!"
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. top: img. bottom
+
         anchors.top: img.bottom
         anchors. topMargin: 5
+
         anchors.topMargin: 5
 
         color: "white"
 
         color: "white"
         font. bold: true
+
         font.bold: true
         font. pointSize: 14
+
         font.pointSize: 14
  
 
     }
 
     }
Line 828: Line 806:
 
These are the different states of the game in short:
 
These are the different states of the game in short:
  
'''* Stopped:'''  This is the default state represented by "" .  This state presten Items as they were declared. Here the mainMenu is visible, the deck is not enabled (no interaction), the end dialog is hidden and timers are stopped.
+
'''* Stopped:'''  This is the default state represented by "" .  This state presten Items as they were declared. Here the mainMenu is visible, the deck is not enabled (no interaction), the end dialog is hidden and timers are stopped.
 
'''* running:'''  In this state the elapsedTimer is running, the deck is enabled so you can pick cards, mainMenu is hidden and end dialog is stopped  
 
'''* running:'''  In this state the elapsedTimer is running, the deck is enabled so you can pick cards, mainMenu is hidden and end dialog is stopped  
 
'''* paused:'''  In this state the elapsedTimer is stopped, the deck is disabled, the mainMenu has a "extended" state and the end dialog is hidden
 
'''* paused:'''  In this state the elapsedTimer is stopped, the deck is disabled, the mainMenu has a "extended" state and the end dialog is hidden
Line 840: Line 818:
 
<code javascript>  
 
<code javascript>  
  
     //Pause Function. Only appears when the game is running
+
     //Pause Function. Only appears when the game is running
 
     Image{
 
     Image{
 
         id:pause
 
         id:pause
         source: "img/pause. PNG"
+
         source: "img/pause.PNG"
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
 
         visible: false
 
         visible: false
 
         MouseArea{
 
         MouseArea{
             anchors. fill: parent
+
             anchors.fill: parent
             onClicked: game. state = "paused"
+
             onClicked: game.state = "paused"
 
         }
 
         }
 
     }
 
     }
Line 860: Line 838:
  
 
<code javascript>   
 
<code javascript>   
// Game. qml
+
// Game.qml
  
import Qt 4. 7
+
import Qt 4.7
  
 
// It represents the game escene
 
// It represents the game escene
Line 882: Line 860:
 
     Image {
 
     Image {
 
         id: crono
 
         id: crono
         source: "img/clock. PNG"
+
         source: "img/clock.PNG"
 
         width: 35
 
         width: 35
 
         height: 35
 
         height: 35
         anchors. top: parent. top
+
         anchors.top: parent.top
         anchors. topMargin: 2
+
         anchors.topMargin: 2
         anchors. left: background. left
+
         anchors.left: background.left
  
 
     }
 
     }
Line 894: Line 872:
 
     Text{
 
     Text{
 
         id: elapsedTimeText
 
         id: elapsedTimeText
         anchors. top: parent. top
+
         anchors.top: parent.top
         anchors. topMargin: 10
+
         anchors.topMargin: 10
         anchors. left: crono. right
+
         anchors.left: crono.right
         anchors. leftMargin: 5
+
         anchors.leftMargin: 5
  
 
     }
 
     }
Line 904: Line 882:
 
     Text {
 
     Text {
 
         id: parCountText
 
         id: parCountText
         anchors. top: parent. top
+
         anchors.top: parent.top
         anchors. topMargin: 10
+
         anchors.topMargin: 10
         anchors. right: background. right
+
         anchors.right: background.right
         anchors. rightMargin: 3
+
         anchors.rightMargin: 3
 
         text: "Pairs Open: "+parCount
 
         text: "Pairs Open: "+parCount
 
     }
 
     }
Line 917: Line 895:
 
         width: 600
 
         width: 600
 
         height: 300
 
         height: 300
         anchors. top: parent. top
+
         anchors.top: parent.top
         anchors. left: parent. left
+
         anchors.left: parent.left
         anchors. topMargin: 40
+
         anchors.topMargin: 40
         anchors. leftMargin: 20
+
         anchors.leftMargin: 20
 
         color: "#6F0564"
 
         color: "#6F0564"
 
         radius: 5
 
         radius: 5
Line 926: Line 904:
 
     }
 
     }
  
     //Pause Function. Only appears when the game is running
+
     //Pause Function. Only appears when the game is running
 
     Image{
 
     Image{
 
         id:pause
 
         id:pause
         source: "img/pause. PNG"
+
         source: "img/pause.PNG"
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
 
         visible: false
 
         visible: false
 
         MouseArea{
 
         MouseArea{
             anchors. fill: parent
+
             anchors.fill: parent
             onClicked: game. state = "paused"
+
             onClicked: game.state = "paused"
 
         }
 
         }
 
     }
 
     }
Line 945: Line 923:
 
         columns: 8
 
         columns: 8
  
         anchors. top: background. top
+
         anchors.top: background.top
         anchors. left: background. left
+
         anchors.left: background.left
  
 
         //Repeater will let us arrange the 32 cards easily
 
         //Repeater will let us arrange the 32 cards easily
Line 979: Line 957:
 
     End{
 
     End{
 
         id: endScreen
 
         id: endScreen
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
 
         visible: false
 
         visible: false
         onSelected: game. state=""      //Move to initial state with mainMenu
+
         onSelected: game.state=""      //Move to initial state with mainMenu
 
     }
 
     }
  
Line 990: Line 968:
 
         id: mainMenu
 
         id: mainMenu
 
         state: "normal"
 
         state: "normal"
         anchors. horizontalCenter: parent. horizontalCenter
+
         anchors.horizontalCenter: parent.horizontalCenter
         anchors. verticalCenter: parent. verticalCenter
+
         anchors.verticalCenter: parent.verticalCenter
 
         onStarted: startGame()
 
         onStarted: startGame()
         onContinued: game. state = "running"
+
         onContinued: game.state = "running"
         onExited: Qt. quit()
+
         onExited: Qt.quit()
 
     }
 
     }
  
Line 1,017: Line 995:
 
         //puts all the cards on its initial state
 
         //puts all the cards on its initial state
 
         for(var i=0;i<32;i++){
 
         for(var i=0;i<32;i++){
             deck. children[i].state=""
+
             deck.children[i].state=""
 
         }
 
         }
  
 
         randomizeCards()
 
         randomizeCards()
  
         game. state="running"
+
         game.state="running"
 
     }
 
     }
  
Line 1,032: Line 1,010:
  
 
         var date = new Date()
 
         var date = new Date()
         var mils = date. getMilliseconds()      //Use miliseconds avoids the same random secuece generation among calls
+
         var mils = date.getMilliseconds()      //Use miliseconds avoids the same random secuece generation among calls
  
 
         for(var i=0;i<32;i++){
 
         for(var i=0;i<32;i++){
 
             var located = false
 
             var located = false
 
             while(!located){
 
             while(!located){
                 var randomnumber = Math. floor((Math. random()*mils)%32)
+
                 var randomnumber = Math.floor((Math.random()*mils)%32)
  
 
                 var content= sortedArray[randomnumber]
 
                 var content= sortedArray[randomnumber]
 
                 if(content !=''){  //if field is already empty, try again
 
                 if(content !=''){  //if field is already empty, try again
                     deck. children[i].parNumber = content
+
                     deck.children[i].parNumber = content
 
                     sortedArray[randomnumber]=''
 
                     sortedArray[randomnumber]=''
 
                     located=true;              //go for the next iteration
 
                     located=true;              //go for the next iteration
Line 1,055: Line 1,033:
  
 
         //Changes the state of the card to open
 
         //Changes the state of the card to open
         deck. children[index].state = "open"
+
         deck.children[index].state = "open"
  
 
         //Storages the index of the card in one of the two placeholders card1 or card2
 
         //Storages the index of the card in one of the two placeholders card1 or card2
Line 1,066: Line 1,044:
  
 
             card2=index
 
             card2=index
             deck. enabled = false    //disables the deck while the pair is closed
+
             deck.enabled = false    //disables the deck while the pair is closed
             closeTimer. start()      //closeTimer is enabled and will close the cards in one second
+
             closeTimer.start()      //closeTimer is enabled and will close the cards in one second
 
         }
 
         }
 
     }
 
     }
Line 1,075: Line 1,053:
  
  
         var parNumber1 = deck. children[card1].parNumber
+
         var parNumber1 = deck.children[card1].parNumber
         var parNumber2 = deck. children[card2].parNumber
+
         var parNumber2 = deck.children[card2].parNumber
 
         var state = ""
 
         var state = ""
  
Line 1,085: Line 1,063:
 
         }
 
         }
  
         deck. children[card1].state = state
+
         deck.children[card1].state = state
         deck. children[card2].state = state
+
         deck.children[card2].state = state
  
 
         //restablish initial values
 
         //restablish initial values
 
         card1 = -1
 
         card1 = -1
 
         card2 = -1
 
         card2 = -1
         deck. enabled = true
+
         deck.enabled = true
 
         parCount++
 
         parCount++
  
Line 1,097: Line 1,075:
 
         //If no remaining pairs, end of the game
 
         //If no remaining pairs, end of the game
 
         if(remaining==0){
 
         if(remaining==0){
         game. state="finished"
+
         game.state="finished"
 
         }
 
         }
 
     }
 
     }
Line 1,111: Line 1,089:
 
         //Arrange the format
 
         //Arrange the format
  
         var seg = date. getUTCSeconds()
+
         var seg = date.getUTCSeconds()
 
         seg = seg >9 ? seg : '0'+seg
 
         seg = seg >9 ? seg : '0'+seg
  
         var mins = date. getUTCMinutes()
+
         var mins = date.getUTCMinutes()
 
         mins = mins>9 ? mins : '0'+mins
 
         mins = mins>9 ? mins : '0'+mins
  
 
         //Display the elapsed time
 
         //Display the elapsed time
         elapsedTimeText. text = mins+":"+seg
+
         elapsedTimeText.text = mins+":"+seg
  
 
     }
 
     }
Line 1,211: Line 1,189:
 
= Source Code and SIS file =
 
= Source Code and SIS file =
  
If you want to check this code, test the game or improve it, here are all the files including a sis file you can install in your phone if you have the Qt 4. 7. 1 libraries on your phone already.
+
If you want to check this code, test the game or improve it, here are all the files including a sis file you can install in your phone if you have the Qt 4.7.1 libraries on your phone already.
  
[[File:QMLMemoryFiles. zip]]
+
[[File:QMLMemoryFiles.zip]]
 
+
[[Category:Code Examples]][[Category:Code Snippet]][[Category:Code Examples]][[Category:Code Snippet]]
+

Revision as of 05:58, 15 February 2012

Featured Article
This article shows how to implement a simple memory game using Qt Quick.


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
  • Whether they match or not, the pair counter will go up 1 unit and this is displayed in the top/right
  • On the top/left corner 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: You will find two options (Start/Exit)
  • When the game is paused: You will find three options (Continue/Restart/Exit)

It also contains an End Dialog box 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 while 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, apart 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 milliseconds in a variable. We use milliseconds because it is easier to construct a Date object with it to give it a proper format. 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 ---
 
//Text indicating the elapsed time on the right of the screen
Text{
id: elapsedTimeText
anchors.top: parent.top
anchors.topMargin: 10
anchors.left: crono.right
anchors.leftMargin: 5
 
}
 
//This timer is triggered every second to keep the timer on the top-left running. It is started manually when the game begins using the start() function
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
 
}
 
}


Main Menu Implementation

we will create a mainMenu in a different file and will refer to it in the game file. The main menu is displayed when the game is stopped or paused. If the game is stopped it will contain only two options: Start and Exit. We will call this state "normal" If the game is paused, it will contain more options: Continue, Restart and Exit. We will call this state "extended" If the game is running, the mainMenu will be hidden, we will call this a "hidden" state.

We will also add a nice transition between the states.

// MainMenu.qml
 
import QtQuick 1.0
 
 
//This item represents the main menu (where user can start or quit the gamde)
Item {
 
id: mainMenu
width: 320
height: 320
 
signal started //emitted when the start button is pressed
signal continued //emitted when the continue button is pressed
signal exited //emitted when the exit button is pressed
 
 
//This rotation transform will allow a nice rotation effect over the x axix
transform: Rotation{
id: contentRotation
origin.x: 160;
origin.y: 0;
axis { x: 1; y: 0; z: 0 }
angle: 0
}
 
 
//Background of the menu with some alpha blending
Rectangle{
width: 320
height: 320
color: "cornflowerblue"
border.width: 3
border.color: "darkblue"
opacity: 0.8
radius: 10
}
 
 
// Positions whatever amount of buttons we have in order in the screen
Grid{
id: grid
rows: 3
columns: 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 15
 
 
// Continue button showed when the game is paused, hidden by default
Rectangle{
 
id: continueButton
width: 200
height:60
radius: 10
 
color: "orange"
border.width: 3
border.color: "orangered"
 
visible: false
 
 
Text {
id: continueText
text: "Continue"
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
font.bold: true
font.pointSize: 12
color: "white"
}
 
MouseArea{
anchors.fill: parent
onClicked: mainMenu.continued() //When pressed the continued signal is emitted
}
}
 
// Start Button presented as "Start" on the beginning or "Restart" when game is paused
Rectangle{
 
id: startButton
width: 200
height:60
 
radius: 10
 
color: "orange"
border.width: 3
border.color: "orangered"
 
visible: false
 
 
Text {
id: startText
text: "Start"
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
font.bold: true
font.pointSize: 12
color: "white"
}
MouseArea{
anchors.fill: parent
onClicked: mainMenu.started() //emits the start signal
}
}
 
 
 
// Exit button
Rectangle{
 
id: exitButton
width: 200
height:60
radius: 10
color: "orange"
border.width: 3
border.color: "orangered"
 
 
Text {
id: exitText
text: "Exit"
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
font.bold: true
font.pointSize: 12
color: "white"
}
 
MouseArea{
anchors.fill: parent
onClicked: mainMenu.exited() //emits the exited signal
}
}
}
 
 
//-------------STATES---------------------
 
states: [
 
//Presented when the game is only started or it has finished
State {
name: "normal"
PropertyChanges{
target: startButton
visible: true
}
PropertyChanges {
target: startText
text: "Start"
}
PropertyChanges {
target: continueButton
visible: false
}
},
 
//Presented when the game is paused
State {
name: "extended"
PropertyChanges{
target: startButton
visible: true
}
 
PropertyChanges {
target: startText
text: "Restart"
}
PropertyChanges {
target: continueButton
visible: true
}
},
 
//Presented when the game is running
State {
name: "hidden"
PropertyChanges {
target: contentRotation
angle: 90
}
}
]
 
 
//-----------TRANSITIONS-----------------
 
//Cool effects of rotation when state is changed to hidden and vice
transitions: [
Transition {
from: "normal"
to: "hidden"
reversible: true
NumberAnimation { target: contentRotation; property: "angle"; duration: 300 }
},
Transition {
from: "extended"
to: "hidden"
reversible: true
NumberAnimation { target: contentRotation; property: "angle"; duration: 300 }
}
]
}


End Dialog

We will also have a dialog indicating the game has come to an end. This dialog will contain a text and an Image and will be dismissed when the user touch it. We will implement it in a different QML file and will access it in the maingame file


// End..qml
 
import QtQuick 1.0
 
 
// This item is shown when all the pairs have been found.
// It means you win!
Item {
 
id: endMessage
width: 320
height: 320
 
signal selected
 
Rectangle {
id: background
color: "green"
opacity: 0.8
width: 320
height: 320
radius: 10
border.width: 3
border.color: "darkgreen"
 
}
 
//Once touched it sends a selected signal
MouseArea{
anchors.fill: parent
onClicked: endMessage.selected()
}
 
Image {
id: img
width: 180
height: 180
source: "img/welldone.PNG"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
 
}
 
Text {
id: name
text: "WELL DONE!"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: img.bottom
anchors.topMargin: 5
color: "white"
font.bold: true
font.pointSize: 14
 
}
}

Game States

Going back to the game and now that we have covered the mainMenu and the end screen, we will describe the different states the game itself has

These are the different states of the game in short:

* Stopped: This is the default state represented by "" . This state presten Items as they were declared. Here the mainMenu is visible, the deck is not enabled (no interaction), the end dialog is hidden and timers are stopped. * running: In this state the elapsedTimer is running, the deck is enabled so you can pick cards, mainMenu is hidden and end dialog is stopped * paused: In this state the elapsedTimer is stopped, the deck is disabled, the mainMenu has a "extended" state and the end dialog is hidden * finished In this state the elapsedTimer is stopped, the deck is disabled, the mainMenu is in "hidden" state and the end dialog is visible


Pausing the Game

To take the game to a paused state we will create a simple button that will be placed on the top/center of the screen

    //Pause Function.  Only appears when the game is running
Image{
id:pause
source: "img/pause.PNG"
anchors.horizontalCenter: parent.horizontalCenter
visible: false
MouseArea{
anchors.fill: parent
onClicked: game.state = "paused"
}
}

Game: complete code

Here is all the code of the game containing the logic described above and the states described above

// Game.qml
 
import Qt 4.7
 
// It represents the game escene
Rectangle {
id: game
width: 640
height: 360
 
 
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 miliseconds //miliseconds elapsed on the game
property int remaining //Indicates how many couples are left to end the game
 
 
 
//Clock icon on the top left corner
Image {
id: crono
source: "img/clock.PNG"
width: 35
height: 35
anchors.top: parent.top
anchors.topMargin: 2
anchors.left: background.left
 
}
 
//Text indicating the elapsed time on the right of clock image
Text{
id: elapsedTimeText
anchors.top: parent.top
anchors.topMargin: 10
anchors.left: crono.right
anchors.leftMargin: 5
 
}
 
//Text indicating the pairs open on the upper left
Text {
id: parCountText
anchors.top: parent.top
anchors.topMargin: 10
anchors.right: background.right
anchors.rightMargin: 3
text: "Pairs Open: "+parCount
}
 
 
//Background where all the cards are located
Rectangle{
id: background
width: 600
height: 300
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 40
anchors.leftMargin: 20
color: "#6F0564"
radius: 5
 
}
 
//Pause Function. Only appears when the game is running
Image{
id:pause
source: "img/pause.PNG"
anchors.horizontalCenter: parent.horizontalCenter
visible: false
MouseArea{
anchors.fill: parent
onClicked: game.state = "paused"
}
}
 
 
//Deck holding the 32 cards
Grid{
id: deck
rows: 4
columns: 8
 
anchors.top: background.top
anchors.left: background.left
 
//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)}
}
}
 
 
//When two cards are open, this timer is enabled to close them in 1 second
Timer{
id:closeTimer
interval: 1000
onTriggered: validatePar()
}
 
//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
 
}
 
//Screen presented when the game is complete, not visible by default
End{
id: endScreen
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
visible: false
onSelected: game.state="" //Move to initial state with mainMenu
}
 
 
//Has the main options of the game, start/quit/about/scores
MainMenu{
id: mainMenu
state: "normal"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
onStarted: startGame()
onContinued: game.state = "running"
onExited: Qt.quit()
}
 
 
//------------ Javascript Functions -----------------------
 
//Init all Variables
function init(){
card1=-1
card2=-1
parCount=0
miliseconds=0
remaining=16
}
 
 
//Starts the Game from the beginning
function startGame(){
 
init() //init variables
 
//puts all the cards on its initial state
for(var i=0;i<32;i++){
deck.children[i].state=""
}
 
randomizeCards()
 
game.state="running"
}
 
 
//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)
 
var content= sortedArray[randomnumber]
if(content !=''){ //if field is already empty, try again
deck.children[i].parNumber = content
sortedArray[randomnumber]=''
located=true; //go for the next iteration
}
}
}
 
}
 
 
//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
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"
}
}
 
 
//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
 
}
 
 
 
// --------Game States ---------------------------
 
// Different game states during its life.
 
// The default state "" indicates a stopped state showing the mainMenu
 
states: [
 
//Indicates the game is running
State {
name: "running"
PropertyChanges {
target: elapsedTimer
running: true
 
}
PropertyChanges {
target: endScreen
visible: false
}
 
PropertyChanges {
target: deck //All the cards are enabled
enabled: true
}
PropertyChanges {
target: mainMenu
state: "hidden"
 
}
PropertyChanges {
target: pause //pause image is shown
visible: true
}
},
 
// Paused state showing the mainMenu with extended options
State {
name: "paused"
PropertyChanges {
target: elapsedTimer
running: false
}
PropertyChanges {
target: deck
enabled: false
}
PropertyChanges{
target: pause
visible:false
}
PropertyChanges {
target: mainMenu
state: "extended"
}
},
 
// Shows the game has reached its end and "Well Done" message is displayed
State {
name: "finished"
PropertyChanges {
target: elapsedTimer
running: false
}
PropertyChanges {
target: deck
enabled: false
}
PropertyChanges {
target: endScreen
visible: true
}
PropertyChanges {
target: mainMenu
state: "hidden"
}
}
]
 
}


Source Code and SIS file

If you want to check this code, test the game or improve it, here are all the files including a sis file you can install in your phone if you have the Qt 4.7.1 libraries on your phone already.

File:QMLMemoryFiles.zip

352 page views in the last 30 days.
×