Tutorials‎ > ‎

[QML] Make Memory Game Animation

posted Dec 18, 2012, 1:50 AM by Muhammad Zullidar   [ updated Aug 15, 2016, 11:24 PM by Surya Wang ]

I. Introduction
  1. What is QML
    QML (QT Modeling Language or QT Meta Language) is a declarative language designed to describe the user interface of a program, both what it looks like, and how it behaves. In QML, a user interface is specified as a tree of objects with properties. QML is JavaScript-based. JavaScript is used as a scripting language in QML, so you must have a little bit experiences about JavaScript if you want to go deeper in QML.

II. QML Elements
  1. QML Graphics There are four QML Graphics elements that we use to make Memory Game. The QML Graphics elements are:
    • Rectangle
      This element is used as container of an image. Actually, it doesn’t use to store your image only, but it also can be used as container of other components, like Text, TextInput, MouseArea, etc.
    • Image
      This element is used to store your images into your application
    • Text
      This element is used to show text
    • Flipable
      This element has two sides that we can use to store QML Elements such as rectangle, image, etc.
  2. QML Animation QML animation is one of the QML elements that used to make animation in your application. QML animation elements that we use to make Memory Game are:
    • SequentialAnimation
      SequentialAnimation allow multiple animations to be run together. Animations defined in SequentialAnimation are run one after the other.
    • ParallelAnimation
      ParallelAnimation allow multiple animations to be run together at the same time.
    • PropertyAnimation
      PropertyAnimation element animates changes in property values.
    • NumberAnimation
      NumberAnimation element animates changes in property values which its value is numeric.
  3. QML States
    • State
      State element defines configurations of objects and properties
  4. QML Transition
    • Transition
      Transition element defines what elements do when state changes.

Those elements will be explained after we start to make Memory Game.
There are many QML elements that we can use to make our application more attractive, interactive, and innovative. If you want to know more about QML elements, you can go to this link.


III. Start Making Memory Game Using QML

Before we start to make main menu, we have to make a container. We use Rectangle element as the container. Here is the code:

Rectangle {
    id: mainScreen
    width: 800
    height: 600
         Component.onCompleted: mainScreen.state = "MainMenu"
}

The code means that we make a rectangle with id = mainScreen. Also to be remembered, id is case-sensitive, so mainScreen and mainscreen are two different ids. We set width and height to 800 and 600. It means your main window is as big as mainScreen size. And also we set mainScreen State to “MainMenu” on Component.onCompleted signal.

  1. Creating Main Menu
    Main Menu

    Here is all of the code to make main menu design:

        /*Main Menu*/
        Rectangle{
            id: startGame
            width: parent.width
            height: parent.height
            anchors.left: parent.left
            color: "#08FC39"
            Text{
                id: mainTitle
                text: "Memory Game"
                color: "#FF7417"
                font{bold: true; family: "Segoe UI Bold"; pointSize: 30}
                anchors{
                    top: parent.top
                    topMargin: 200
                    horizontalCenter: parent.horizontalCenter
                }
            }
            Text{
                id: txtStartGame
                text: "Start Game"
                anchors{
                    horizontalCenter: parent.horizontalCenter
                    verticalCenter: parent.verticalCenter
                }
                color: "White"
                font{bold: true; family: "Segoe UI Bold"; pointSize: 15}
                MouseArea{
                    id: maStartGame
                    anchors.fill: parent
                    hoverEnabled: true
                    onEntered: txtStartGame.color = "#6017FF"
                    onExited: txtStartGame.color = "white"
                    onClicked: {
                        Script.randomPict();
                        mainScreen.state="StartGame"
                    }
                }
            }
            Text{
                id: exit
                text: "X"
                anchors{
                    top: parent.top
                    right: parent.right
                    rightMargin: 10
                }
                font{bold: true; pointSize: 30}
                MouseArea{
                    anchors.fill: parent
                    onClicked: Qt.quit()
                }
            }
        }
        /*------------------------------*/
    
    • [Line 2-4] We make a rectangle as container with id = startGame. startGame’s width and height we set as big as its parent, which is mean, the mainScreen. We can use keyword parent to access parent’s attribute.
    • [Line 5] Set the left anchors to the left-most of the parent.
    • [Line 6] Set the color of the rectangle with hexadecimal “#08FC39”
    • [Line 7-11] Make Text element to show the title of “Memory Game”. The Text element’s id is mainTitle. Its color is set with hexadecimal “#FF7417”. Its font set to bold, with "Segoe UI Bold" family and set pointSize (the size of the text) to 30. We also set the mainTitle’s top anchors to parent’s top. But, we also set the topMargin, the margin from the top of the element, to 200. Set the horizontalCenter anchors to parent’s horizontalCenter, which mean, it will on the center of horizontal of the startGame element.
    • [Line 12-32] Make Text element to show “Start Game” text. We set its id to txtStartGame . Set the horizontalCenter anchors to its parent’s horizontalCenter and verticalCenter anchors set to its parent’s verticalCenter. We set its color to “white”. Set its font to bold, with "Segoe UI Bold" family, and set pointSize to 15. We want to set txtStartGame clickable, so we add MouseArea. We set its id to maStartGame. We want to make MouseArea cover whole part of txtStartGame, so we set its anchors.fill to its parent, which means txtStartGame itself. We want to add some hover animation on txtStartGame, so we have to set hoverEnabled to true. And set what txtStartGame will do when mouse enter txtStartGame and exit from txtStartGame. We use onEntered signal to do that. On onEntered signal, we set txtStartGame’s color to "#6017FF", and on onExited signal, we set txtStartGame’s color back to white. After that, to make txtStartGame clickable, we have to add onClicked signal. We set what txtStartGame will do after we click txtStartGame. In this case, I set the signal with javascript code Script.randomPict() and set mainScreen state to “StartGame”, which I’ll explain later.
    • [Line 33-46] Make Text element to show “X” symbol on the right-top side of the screen. It use for exit from the game. We set its id to exit. We also set its top anchors to its parent’s top, right anchors to its parent’s right, and set rightMargin to 10. We set its font to bold, and set its pointSize to 30. We also want to make it clickable, so add MouseArea element. We set the anchors.fill of the MouseArea element to its parent, so it will cover whole part of exit. And on onClicked signal, we set to Qt.quit(), which means it will close the application.

  2. Creating the Start Game state
    Start Game Page 1

    Start Game Page 2

    Here is all of the code:

    /*StartGame*/
        Text{
            id: startGameTitle
            visible: false
            anchors{
                top: mainScreen.top
                topMargin: 30
                horizontalCenter: mainScreen.horizontalCenter
            }
            text: "Choose This Picture"
            color: "white"
            font{bold:true; family: "Segoe UI Bold"; pointSize: 20}
        }
        Image{
            id: thePicture
            visible: false
            width: 70
            height: 70
            anchors{
                left: startGameTitle.right
                leftMargin: 20
                verticalCenter: startGameTitle.verticalCenter
            }
            source: ""
        }
        Flipable{
            id:card1
            width: 150
            height: width
            visible: false
            x:-width
            y:100
            enabled: false
            transform: Rotation {
                id: rot1
                origin.x: card1.width/2
                origin.y: 0
                axis.x: 0; axis.y: 1; axis.z: 0;
                angle: 0
                onAngleChanged: img1.source = "../../images/models/"+Script.getPict(0)+".jpg";
                Behavior on angle {PropertyAnimation{}}
            }
            front:
                Rectangle {
                    color: "#FAFF70"
                    width: 150
                    height: width
                    MouseArea {
                        anchors.fill: parent;
                        onClicked: {
                            cekPict1.start();
                        }
                    }
            }
            back:
                Image {
                    id: img1
                    width: 150
                    height: width
            }
        }
        Flipable{
            id:card2
            width: 150
            height: width
            visible: false
            x: mainScreen.width
            y: 100
            enabled: false
            transform: Rotation {
                id: rot2
                origin.x: card2.width/2
                origin.y: 0
                axis.x: 0; axis.y: 1; axis.z: 0;
                angle: 0
                onAngleChanged: img2.source = "../../images/models/"+Script.getPict(1)+".jpg";
                Behavior on angle {PropertyAnimation{}}
            }
            front:
                Rectangle {
                    color: "#FAFF70"
                    width: 150
                    height: width
                    MouseArea {
                        anchors.fill: parent;
                        onClicked: {
                            cekPict2.start();
                        }
                    }
            }
            back:
                Image {
                    id: img2
                    width: 150
                    height: width
            }
        }
        Flipable{
            id:card3
            width: 150
            height: width
            visible: false
            x: -width
            y: mainScreen.height - width - 50
            enabled: false
            transform: Rotation {
                id: rot3
                origin.x: card3.width/2
                origin.y: 0
                axis.x: 0; axis.y: 1; axis.z: 0;
                angle: 0
                onAngleChanged: img3.source = "../../images/models/"+Script.getPict(2)+".jpg";
                Behavior on angle {PropertyAnimation{}}
            }
            front:
                Rectangle {
                    color: "#FAFF70"
                    width: 150
                    height: width
                    MouseArea {
                        anchors.fill: parent;
                        onClicked: {
                            cekPict3.start();
                        }
                    }
            }
            back:
                Image {
                    id: img3
                    width: 150
                    height: width
            }
        }
        Flipable{
            id:card4
            width: 150
            height: width
            visible: false
            x: mainScreen.width
            y: mainScreen.height - width - 50
            enabled: false
            transform: Rotation {
                id: rot4
                origin.x: card4.width/2
                origin.y: 0
                axis.x: 0; axis.y: 1; axis.z: 0;
                angle: 0
                onAngleChanged: img4.source = "../../images/models/"+Script.getPict(3)+".jpg";
                Behavior on angle {PropertyAnimation{}}
            }
            front:
                Rectangle {
                    color: "#FAFF70"
                    width: 150
                    height: width
                    MouseArea {
                        anchors.fill: parent;
                        onClicked: {
                            cekPict4.start();
                        }
                    }
            }
            back:
                Image {
                    id: img4
                    width: 150
                    height: width
            }
        }
        /*------------------------------*/
    
    • [Line 1-12] Make Text element to show “Choose this picture” text. Its id is startGameTitle. We set its visible properties to false, because we don’t need this element when the program runs at the first time. Set its top anchors to top of the mainScreen, topMargin to 30, and horizontalCenter to horizontalCenter of mainScreen. Set the color to “white”. Set the font to bold, with "Segoe UI Bold" family, and set its pointSize to 20.
    • [Line 13-24] Make an Image element to show a picture that we have to choose. We set its id to thePicture. Set its visible to false. Set its width and height to 70. Set its left anchors to the right of startGameTitle, set leftMargin to 20, and set verticalCenter to the verticalCenter of startGameTitle. We set source to empty string, because we want to fill it at runtime.
    • [Line 25-168] All elements of this line are Flipable element. This element has two different sides, back side and the front side. We can adjust the side as we needs. In this case, I set the front side as Rectangle, and the backside as Image. To make it rotatable, we set transform property with Rotation element. Because we want to flip the card to the y-axis, so we set origin coordinate of x to the result of card width divided by 2. Set origin coordinate y to 0. Set axis.x to 0, axis.y to 1, and axis.z to 0. We set the angle to 0. On onAngleChanged signal, we set the source of the image (back-side). So every Flipable rotate, the image of Image element will be set. We also set Behavior of the angle with PropertyAnimation, so when the angle changes, we do animation to the Flipable. Because we want to make four cards, so we just copy paste the code, and adjust the default position and the id of the card. You can see the position at the code above.

  3. Creating End Game State
    You Win Page

    You Lose Page

    Here is all of the code:

    /*End Game*/
        Rectangle{
            id: blackRect
            visible: false
            opacity: 0
            color: "black"
            width: mainScreen.width
            height: mainScreen.height
        }
        Text{
            id: blackRectText
            color: "#FF7417"
            visible: false
            anchors{
                horizontalCenter: blackRect.horizontalCenter
                verticalCenter: blackRect.verticalCenter
            }
            font{bold:true; family: "Segoe UI Bold"; pointSize:50}
        }
        Text{
            id: playAgain
            color: "white"
            visible: false
            text: "Play Again"
            anchors{
                horizontalCenter: blackRect.horizontalCenter
                top: blackRectText.bottom
                topMargin: 20
            }
            font{bold:true; family: "Segoe UI Bold"; pointSize:30}
            MouseArea{
                anchors.fill: parent
                hoverEnabled: true;
                onEntered: playAgain.color = "#6017FF"
                onExited: playAgain.color = "white"
                onClicked: mainScreen.state = "MainMenu"
            }
        }
        /*------------------------------*/
    
    • [Line 1-8] Make Rectangle to show the black rectangle cover whole part of the games.Set its id to blackRect. We want to make an effect on it. So when the game is over, the black rectangle’s opacity will increase smoothly. But first, we set visible property to false as default values. We don’t want to show it at first run of the program. We also set the opacity to 0, color to “black”, width and height value as big as mainScreen.
    • [Line 9-18] Make Text element to show status of the user, if he/she win or lose. We don’t set the text because we want to set it at runtime. Set its id to blackRectText. Set the color to “#FF7417”. Set the visible to false, because we don’t to show it at the first time the program run. We set horizontalCenter anchors to the horizontalCenter of blackRect. And also we set verticalCenter anchors to the verticalCenter of the blackRect. We also set its font to bold, with "Segoe UI Bold" family, and its pointSize to 50.
    • [Line 19-37] Make Text element to show “Play Again” text. We set its id to playAgain. Set the color to “white”. Set visible to false. Set horizontalCenter anchors to horizontalCenter of blackRect, set top anchors to bottom of blacRectText, set top margin to 50. Set the font to bold, with "Segoe UI Bold" family, and set its pointSize to 30. Because we want to make this element clickable, so we add MouseArea element. We set anchors.fill to its parent (playAgain). Because we want to add animation while mouse hovering the text, so we have to set hoverEnabled to true. On onEntered signal, we set playAgain’s color to “#6017FF”. On onExited signal, we set playAgain’s color back to “white” again. On onClicked signal, we set mainScreen’s state to “MainMenu”.

After all of the design already made, now we set the state and transition that happened after the state changed.


  1. State

    Here is all the code of the state. We place it in mainScreen scope:

    states:[
            State{
                name: "MainMenu"
                PropertyChanges{
                    target: startGame
                    width: mainScreen.width
                }
                PropertyChanges{
                    target: mainTitle
                    visible: true
                }
                PropertyChanges{
                    target: blackRect
                    visible: false
                }
                PropertyChanges{
                    target: blackRectText
                    visible: false
                }
                PropertyChanges{
                    target: playAgain
                    visible: false
                }
                PropertyChanges {
                    target: thePicture
                    visible: false
                }
            },
            State{
                name: "StartGame"
                PropertyChanges{
                    target: rot1
                    angle: 0
                }
                PropertyChanges{
                    target: rot2
                    angle: 0
                }
                PropertyChanges{
                    target: rot3
                    angle: 0
                }
                PropertyChanges{
                    target: rot4
                    angle: 0
                }
                PropertyChanges{
                    target: thePicture
                    visible: false
                }
                PropertyChanges {
                    target: blackRect
                    visible: false
                }
                PropertyChanges {
                    target: blackRectText
                    visible: false
                }
                PropertyChanges {
                    target: playAgain
                    visible: false
                }
                PropertyChanges {
                    target: startGame
                    width: mainScreen.width
                }
                PropertyChanges {
                    target: mainTitle
                    visible: false
                }
                PropertyChanges {
                    target: startGameTitle
                    visible: true
                }
                PropertyChanges {
                    target: txtStartGame
                    visible: false
                }
                PropertyChanges{
                    target: maStartGame
                    enabled: false
                }
                PropertyChanges {
                    target: card4
                    visible: true
                    x: 550
                }
                PropertyChanges {
                    target: card2
                    visible: true
                    x: 550
                }
                PropertyChanges {
                    target: card3
                    visible: true
                    x: 100
                }
                PropertyChanges {
                    target: card1
                    visible: true
                    x: 100
                }
            }
        ]
    

    The states property is belong to mainScreen. It contains two State elements.

    • First State element named “MainMenu”. We use PropertyChanges to set what property will change if the state is running. We set visibility of blackRect, blackRectText, playAgain, thePicture to false. Set mainTitle visibility to true.and set startGame width to mainScreen’s width.
    • Second State element named “StartGame”. We use this state when user clicks text “Start Game”. You can see what happened in “StartGame” state on code above.

  2. Transitions

    Transition is occurred when state changing from one to another. We do animation in every transition. Here is the code:

    transitions:[
            Transition {
                from: "MainMenu"
                to: "StartGame"
                SequentialAnimation{
                    PropertyAnimation{
                        properties: "x"
                        duration: 500
                    }
                    PauseAnimation { duration: 200 }
                    ParallelAnimation{
                        PropertyAnimation{target: rot1; property: "angle"; from:0; to: 180; duration: 1000}
                        PropertyAnimation{target: rot2; property: "angle"; from:0; to: 180; duration: 1000}
                        PropertyAnimation{target: rot3; property: "angle"; from:0; to: 180; duration: 1000}
                        PropertyAnimation{target: rot4; property: "angle"; from:0; to: 180; duration: 1000}
                    }
                    PauseAnimation { duration: 5000 }
                    ParallelAnimation{
                        PropertyAnimation{target: rot1; property: "angle"; from:180; to: 0; duration: 1000}
                        PropertyAnimation{target: rot2; property: "angle"; from:180; to: 0; duration: 1000}
                        PropertyAnimation{target: rot3; property: "angle"; from:180; to: 0; duration: 1000}
                        PropertyAnimation{target: rot4; property: "angle"; from:180; to: 0; duration: 1000}
                    }
                    PauseAnimation { duration: 1000 }
                    ScriptAction{
                        script: Script.randomAnimation();
                    }
                    PauseAnimation { duration: 2000 }
                    ScriptAction{
                        script: Script.randomAnimation();
                    }
                    PauseAnimation { duration: 2000 }
                    ScriptAction{
                        script: Script.randomAnimation();
                    }
                    PauseAnimation { duration: 2000 }
                    ScriptAction{
                        script: Script.randomAnimation();
                    }
                    PauseAnimation { duration: 2000 }
                    ScriptAction{
                        script: Script.randomAnimation();
                    }
                    PauseAnimation { duration: 1000 }
                    ScriptAction{
                        script: {
                            card1.enabled = true;
                            card2.enabled = true;
                            card3.enabled = true;
                            card4.enabled = true;
                            thePicture.source = "../../images/models/"+Script.getRandPict()+".jpg";
                            thePicture.visible = true;
                        }
                    }
                }
            }
        ]
    

    When state change from “MainMenu” to “StartGame”, so we do SequentialAnimation. The SequentialAnimation has many other animations in it. First animation is, we do animation while property “x” changing. We give duration 500 ms. And then, we add PauseAnimation to give waiting time for 200ms. After that we do ParallelAnimation, where the card rotate with angle 180 degree. And after that, we add PauseAnimation for 5000ms. And then we do ParallelAnimation again to re-flip the card. We do rotation with angle 0 degree. We add PauseAnimation to set waiting time before we start next animation. In the next animation, we add ScriptAction. ScriptAction is an element that allow us to do scripting here with javascript. In this animation, I add 6 ScriptAction, where the first five animations are to run Script.randomAnimation() which I will explain later. And the last ScriptAction is to enabled all of the cards, so the cards will be clickable. And also we set the picture that user have to choose. And we set visibility of the picture to true.

  3. Creating Javascript functions

    To make this game more attractive and interactive, I add some javascript functions. Here is all of the code:

    var pict = new Array(0,0,0,0);
    var randPict = 0;
    function randomPict(){
            var i=0;
            var idx = 0;
            for(i=0;i<4;i++){
                if(i==0)
                {
                    idx = Math.ceil(Math.random()*12);
                    pict[i] = idx;
                }
                else
                {
                    var flag = 0;
                    while(flag == 0)
                    {
                        var a = 0;
                        idx = Math.ceil(Math.random()*12);
                        for(var j=0;j<i;j++)
                        {
                            if(pict[j] == idx)
                            {
                                a = 1;
                                break;
                            }
                        }
                        if(a==0)
                        {
                            pict[i] = idx;
                            flag = 1;
                        }
                    }
                }
            }
        }
        function getRandPict(){
            var idx = Math.ceil(Math.random()*4)-1;
            randPict = pict[idx];
            return randPict;
        }
        function getPict(idx){
            return pict[idx];
        }
        function randomAnimation(){
            var flag = true;
            while(flag)
            {
                switch(Math.ceil(Math.random()*6))
                {
                    case 1:
                        if(card1.y == card3.y && card2.y == card4.y)
                        {
                            anim1.start();
                            flag = false;
                        }
                        break;
                    case 2:
                        if(card1.x == card3.x && card2.x == card4.x)
                        {
                            anim2.start();
                            flag = false;
                        }
                        break;
                    case 3:
                        if(card2.x == card4.x)
                        {
                            anim3.start();
                            flag = false;
                        }
                        break;
                    case 4:
                        if(card1.x == card3.x)
                        {
                            anim4.start();
                            flag = false;
                        }
                        break;
                    case 5:
                        if(card1.y == card2.y)
                        {
                            anim5.start();
                            flag = false;
                        }
                        break;
                    case 6:
                        if(card3.y == card4.y)
                        {
                            anim6.start();
                            flag = false;
                        }
                        break;
                }
            }
        }
    function cekPict(a)
    {
        if(a === randPict)return true;
        else return false;
    }
    
    • We make an array variable called pict. This variable is used to store the number of the images that we would use in the game, where they were randomized when we play the game.
    • We make a variable called randPict that contains of the number of the picture that user have to choose
    • We make a function called randomPict() that used to random all the pictures from disk, and store the number to the pict variable. The value of pict variable are the name of the pictures, where the name of the picture is a [number].jpg (you can see it in the project I attached below).
    • We make a function called getRandPict(). This function is used to get random picture as a picture that user have to choose.
    • We make a function called getPict(idx). This function is used to get a specific picture that we will store it on the card. The idx means the index of the card (if card1, then idx=0. If card2, then idx=1).
    • We make function called randomAnimation() . This function is used to random the movement animation of the card. It will make your card movement animation not always same when you play the game several time.
    • We make function called function cekPict(a). This function is used to check the picture that user have choose. It returns Boolean value whether the picture and user’s picture is same picture or no.

    You can check all of the code to see what happened on the functions.

This is the end of the tutorial “Make Memory Game with Animation Using QML”. Hope you understand the tutorial and can help you to make better game or application. Thank you.

ċ
NewMemoryGame.zip
(5393k)
Muhammad Zullidar,
Dec 18, 2012, 1:50 AM