summaryrefslogtreecommitdiffstats
path: root/quick3d/chapter07/index.rst
blob: 034399acc6ded491746ad57b844f74f336c618db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
..
    ---------------------------------------------------------------------------
    Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
    All rights reserved.
    This work, unless otherwise expressly stated, is licensed under a
    Creative Commons Attribution-ShareAlike 2.5.
    The full license document is available from
    http://creativecommons.org/licenses/by-sa/2.5/legalcode .
    ---------------------------------------------------------------------------

Game Menu
=========

In this chapter we will be implementing the game menu. With Qt Quick it's easy to mix 2D and 3D elements which enables us to add basic UI to our game.


Head-up display
---------------

A Head-up display (HUD) usually shows information to the player about the current game state and the player's conditions. There are actually three things we want to display: the level of the laser's energy, the hit points and the score.

So first we add the following properties to the player:

.. code-block:: js

    //Player.qml
    ...
    property int hitpoints
    property real maxHitPoints: 10

    property int energy
    property int maxEnergy: 2000
    ...

Then we want to display two `energy` bars. A red one in the center to show the player's hit points and a blue one to show the laser's energy left. The current score is displayed in the upper left corner of the viewport. To archieve that, we add a new `Hud.qml` file that consist of an `Item` containing two `Rectangles` that present the bars, and a `Text` element to display the score.


.. code-block:: js

    //Hud.qml
    import QtQuick 2.0

    Item {
        id: hud
          anchors.fill: parent

          Text {
              anchors.left: parent.left
              anchors.top: parent.top
              anchors.margins: 10
              text: "Score: " + score;
              style: Text.Raised
              font.pixelSize: 20
              color: "green"
          }

          Rectangle {
              anchors.top: parent.top
              anchors.topMargin: 20
              anchors.horizontalCenter: parent.horizontalCenter
              width: parent.width/2
              height: 15
              color: "transparent"
              border.color: "red"
              Rectangle{
                    anchors.left: parent.left
                    anchors.top: parent.top
                    anchors.bottom: parent.bottom
                    width: parent.width*player.hitpoints/player.maxHitPoints;
                    color: "red"
              }
          }

          Rectangle {
              anchors.right: parent.right
              anchors.rightMargin: 20
              anchors.verticalCenter: parent.verticalCenter
              height: parent.height/3
              width: 10
              color: "transparent"
              border.color: "blue"
              Rectangle{
                    anchors.right: parent.right
                    anchors.left: parent.left
                    anchors.bottom: parent.bottom
                    height: parent.height*player.energy/player.maxEnergy;
                    color: "blue"
              }
          }
  }

Then we instantiate the `HUD` in `game.qml` as follows:

.. code-block:: js

  //game.qml
  ...
  Viewport {
    ...
    //Head up display
    Hud {id: hud}
    ...
  }

Game menu
---------

Once the game is started, a menu should be displayed. This menu consists of a button group containing three buttons: `start`, `highscore` and `exit`. While the menu is displayed, the     hamburger* is rotating in the background. When clicking on the "start" button, the game starts and the camera is moved behind the *hamburger*. When clicking on the "highscore" button, a new rectangle will appear and displays the highscores in a `ListView`. To exit the game the player can simply click on the *exit* button.

.. image:: img/menu.png
    :scale: 80%
    :align: center

Before we start implementing the menu, we first have to define two missing camera movements. One is the rotation of the     hamburger* while the game menu is displayed and the other moves the camera behind the *hamburger* when we start the game:

.. code-block:: js

    //game.qml
    ...
    //The game camera
    camera: Camera {
        id: cam
        property real angle:0;
        eye: Qt.vector3d(20    Math.sin(angle), 10, 20*Math.cos(angle))
        NumberAnimation on angle{
            id: hamburgerRotation
            to: 100
            running: false
            duration: 1000000;
        }
        PropertyAnimation on eye {
            id: moveBehindHamburger
            to: Qt.vector3d(0, 0,-30)
            duration: 2000
            running: false
        }
    }
    ...

Then we define a new button component in a new `Button.qml` file:

.. code-block:: js

    //Button.qml
    import QtQuick 2.0

    //Creates a simple button that has an attribute buttonText
    Rectangle {
          id:rootI
          width: 200;
          height: 50;
          signal buttonClicked();
          property variant buttonText;
          radius: 5
          border.color: "black"
          border.width: 2
          color: "darkblue"
          opacity: 1
          MouseArea {
              hoverEnabled: true;
              anchors.fill: parent;
              onClicked: buttonClicked();
              onEntered: border.color="white"
              onExited: border.color="black"
          }
          Text {
              anchors.centerIn: parent;
              text:  buttonText;
              color: "white"
          }
    }

Next we create our menu component in a `Menu.qml` file. The menu consists of an `Item` with a `Column` containing three buttons. When a button is clicked, the appropriate state will be set in the root element (the viewport):

.. code-block:: js

  //Menu.qml
  import QtQuick 2.0

  Item {
      visible: false
      anchors.fill: parent
      //The button group
      Column {
          id: buttonGroup
          anchors.verticalCenter: parent.verticalCenter;
          anchors.left: parent.left;
          anchors.leftMargin: 20
          spacing: 10

        Button {
            buttonText: "Start game"
            onButtonClicked: root.state="Game"
        }

        Button {
            buttonText: "Highscore"
            onButtonClicked: root.state="Highscore"
        }

        Button {
            buttonText: "Exit"
            onButtonClicked: Qt.quit()
        }

    }
  }

Then we add the menu to the `Viewport` in `game.qml`.

.. code-block:: js

  //game.qml
  ...
  Viewport {
    ...
    Menu {id: gamemenu}
    ...
  }

To save the highscore table, we will use an `SQLite` database. We will avoid discussing the detail how to `SQLite` in QML. For more detail please refer to the `Qt Quick Desktop Guide <http://qt.nokia.com/learning/guides>`_.

For that, we create a new `gameDB.js` Stateless JavaScript library. This means that only one instance will be created for all QML file including it. The library defines the database logic as shown in the code below:

.. code-block:: js

  // gameDB.js

  //making the gameDB.js a stateless library
  .pragma library

  .import QtQuick.LocalStorage 2.0 as Sql

  // declaring a global variable for storing the database instance
  var _db

  //Opens the database connection
  function openDB() {
      print("gameDB.createDB()")
      _db = Sql.openDatabaseSync("SpaceburgerDB","1.0","The Spaceburger Database"
        ,1000000);
      createHighscoreTable();
  }

  //Creates the highscore table
  function createHighscoreTable() {
      print("gameDB.createTable()")
      _db.transaction( function(tx) {
    tx.executeSql("CREATE TABLE IF NOT EXISTS "
        +"highscore (score INTEGER, name TEXT)");
              });
  }

  //Reads the first 10 elements of the highscoretable and returns them as an array
  function readHighscore() {
      print("gameDB.readHighscore()")
      var highscoreItems = {}
      _db.readTransaction( function(tx) {
              var rs = tx.executeSql("SELECT name, score FROM "
             +"highscore ORDER BY score DESC LIMIT 0,10");
              var item
              for (var i=0; i< rs.rows.length; i++) {
          item = rs.rows.item(i)
          highscoreItems[i] = item;
              }
          });

      return highscoreItems;
  }

  //Saves an element into the highscore table
  function saveHighscore(score, name) {
      print("gameDB.saveHighscore()")
      _db.transaction( function(tx){
          tx.executeSql("INSERT INTO highscore (score, name) "
         +"VALUES(?,?)",[score, name]);
              });

  }


Next we create the highscore table in `Menu.qml`:

.. code-block:: js

  //Menu.qml
  Item {
    ...
    ListModel {
        id: highscoreModel;
    }

    Component.onCompleted: {
        GameDB.openDB();
    }

    Rectangle {
        visible: root.state=="Highscore"
        anchors.left: buttonGroup.right
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.top: parent.top
        anchors.margins: 50
        radius: 5
        border.color: "black"
        border.width: 2
        color: "darkblue"
        opacity: 0.7
        Text {
            id: title
            anchors.top: parent.top
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.topMargin: 20
            text: "Highscore"
            font.bold: true
            font.pointSize: 15
            color: "white"
        }
        //The highscore table
        ListView {
              id: highscore
            anchors.top: title.bottom
            anchors.topMargin: 50
            anchors.verticalCenter: parent.verticalCenter
            width: parent.width-70
            height: parent.height-title.height-50
            model: highscoreModel;
            delegate: Item {
                anchors.left: parent.left; anchors.right: parent.right
                    anchors.margins: 40
                height: 30
                Text{anchors.left: parent.left;   text: name;  font.bold: true;
                    font.pointSize: 20; color: "white"}
                Text{anchors.right: parent.right; text: score; font.bold: true;
                    font.pointSize: 20; color: "white"}
            }
        }
     }
  }

As you might have noticed, we have created an empty `ListModel` and used it in the `ListView`. Next we are going to populate this model with the data we get out of the SQL table through the `readHighscore()` function.

The first thing to do is to import the library:

.. code-block:: js

  //Menu.qml
  import "gameDB.js" as GameDB

Now we can read the data from the highscore table. We will do that in the `onVisibleChanged` signal handler of the highscore item, so that an update will occur every time the highscor is displayed.

We use the `GameDB's` `readHighscore()` function to read the highscore table from the databse parse it into the `ListModel` we have already defined:

.. code-block:: js

  //Menu.qml
  ...
        onVisibleChanged: {
            if (visible == true) {
                var highscoreTable=GameDB.readHighscore();
                highscoreModel.clear();
                for (var i in highscoreTable) {
                    print(highscoreTable[i])
                    highscoreModel.append(highscoreTable[i]);
                }
            }
        }

Adding a new highscore into the SQL table is possible once the game has been finished. A dialog is displayed that asks the player to enter his name. The name and the score will then be saved. The code of the dialog is implemeted into `HighscoreDialog.qml` as follows:

.. code-block:: js

  //HighscoreDialog.qml
  import QtQuick 2.0
  import "gameDB.js" as GameDB

  Rectangle{
      anchors.verticalCenter: root.verticalCenter
      anchors.horizontalCenter: root.horizontalCenter
      height:170
      width:270
      radius: 5
      border.color: "black"
      border.width: 2
      color: "darkblue"
      opacity: 0.7
      visible: false
      Text{
          id: title
          anchors.horizontalCenter: parent.horizontalCenter
          anchors.top: parent.top
          anchors.topMargin: 15
          text: "Enter your name:"
          font.pointSize: 17
          color: "white"
      }

      Rectangle{
          id: input
          anchors.horizontalCenter:  parent.horizontalCenter
          anchors.top:  title.bottom
          anchors.topMargin: 15
          height: 40
          width: 200
          radius: 2
          color: "lightgray"
          clip: true
          TextInput{
              id: inputField
              anchors.fill: parent
              color: "black"
              text: "Name..."
              font.pointSize: 17
          }
      }

      Button {
          anchors.bottom: parent.bottom;
          anchors.bottomMargin: 15
          anchors.right: parent.right
          anchors.rightMargin: 15
          buttonText: "OK"
          onButtonClicked: {
              GameDB.saveHighscore(score, inputField.text)
              root.state="Menu"
          }
      }
  }

.. code-block:: js

  //main.qml
  ...
  HighscoreDialog {id: highscoreDialog}
  ...

We now can update our states:

.. code-block:: js

    states:[
        State{
            name: "Menu"
            PropertyChanges {target: player; ax: 0; ay: 0; vx: 0; vy:0;
                                position: Qt.vector3d(0, 0, 0);  hitpoints: 2;
                                energy:2000;  restoreEntryValues: false}
            PropertyChanges {target: root; score: 0; targetCount:0;
                                restoreEntryValues: false}
            PropertyChanges {target: cam; center: Qt.vector3d(0, 0, 0) }
            PropertyChanges {target: gamemenu; visible: true;}
            PropertyChanges {target: hamburgerRotation; running: true;}
            PropertyChanges {target: hud; visible: false;}
        },
        State{
            name: "Highscore"
            extend: "Menu"
        },
        State{
            name: "EnterHighscore"
            PropertyChanges {target: hud; visible: true;}
            PropertyChanges {target: highscoreDialog; visible: true;}
        },
        State{
            name: "Game"
            PropertyChanges {target: moveBehindHamburger; running: true;}
            PropertyChanges {target: hud; visible: true;}
        },
        State{
            name: "BossFight"
            PropertyChanges {target: hud; visible: true;}
            PropertyChanges {target: player; ay: 0; vy:0;
        position: Qt.vector3d(0, 0, 0); restoreEntryValues: false}
        },
        State{
            name: "BossRotation"
        }
    ]

.. rubric:: What's Next?

Next we implement the boss enemy that should appear at the final level.