Ma première application Android avec Qt : partie 2

Utilisez Qt pour développer sur Android

Michael Bertocchi

Dans l'article précédent, nous avons installé les prérequis : SDK, EDI et bibliothèques ; dans cette seconde partie, nous allons entrer dans le vif du sujet : l'écriture d'un jeu sur mobile.

- Titre : Ma première application Android avec Qt : partie 2

- Auteur : Michael Bertocchi

- Parution : 6 décembre 2016

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Michael Bertocchi. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Présentation générale du jeu

- III. Résultat attendu : à quoi ressemblera notre gâteau

- IV. Présentation de nos ustensiles de cuisine

- IV-A. StackView

- IV-B. Modèles et ListModel

- IV-C. Repeater

- IV-D. Timer

- IV-E. AnimatedSprite

- V. Préparation à cuisiner

- V-A. Création d'une application Android dans Qt

- V-B. Avant de commencer

- V-B-1. Réflexion et organisation avant l'action

- V-B-2. Un jeu adapté à tous les écrans

- V-C. Installation de la base de notre application

- V-C-1. Préfixes

- V-C-2. Modification du fichier QML principal

- V-C-3. Ajout du moteur du jeu : le fichier JavaScript

- V-C-4. Ajout des pages

- V-C-5. Ajout des images pour les pages

- V-D. Ajout de la navigation générale

- V-D-1. Édition des écrans de navigation

- V-D-2. Ajout de la mécanique de changement de pages

- V-E. Un petit aperçu dès le début de ce projet

- VI. À vos fourneaux

- VI-A. Ajout d'une pluie d'ennemis

- VI-A-1. Préparation

- VI-A-2. Intégration du composant Enemy à notre application

- VI-A-3. Permettre l'appel d'ajout d'ennemis à l'écran

- VI-B. Amélioration de cette pluie pour qu'elle arrose bien tout l'écran

- VI-C. Ajout d'interactions sur nos ennemis

- VI-D. Ajout d'un module score et vies

- VI-D-1. Création du composant de score

- VI-D-2. Inclusion dans la scène

- VI-D-3. Ajout dans le fichier JavaScript du lien vers le score

- VI-D-4. Implémentation du mécanisme des vies

- VI-D-5. Inclusion sur la scène

- VI-D-6. Ajout du lien dans le fichier JavaScript vers ce composant de gestion de vies

- VI-E. Ajout du lien entre les ennemis et nos modules score et vies

- VII. Finitions

- VII-A. Ajout d'une animation sur les ennemis : découverte des sprites

- VII-B. Ajout d'explosions à la suppression des ennemis

- VII-C. Amélioration du score : effet graphique

- VII-D. Amélioration de nos boutons

- VII-E. Un peu de profondeur avec des nuages en fond

- VIII. Conclusion

- IX. Remerciements

I - Introduction

Nous avions vu, dans la première partie, comment installer le framework Qt ainsi que le SDK Android.

Dans cette seconde partie, nous allons entrer dans le vif du sujet en réalisant un simple jeu de type « shoot them up ».

Le schéma didactique choisi prendra la forme d'une recette de cuisine : présentation du gâteau, des instruments, liste des courses…

II - Présentation générale du jeu

Le jeu que nous allons développer est un simple « shoot them up » : des cibles descendent du haut de l'écran et il suffit de cliquer dessus (avec la souris ou le doigt) pour les supprimer et augmenter le score.

Pour cela, nous allons réaliser plusieurs choses :

- créer des cibles dynamiquement et régulièrement ;

- les afficher à l'écran ;

- créer un événement sur le clic de l'un d'entre eux ;

- créer un événement quand une cible arrive en bas de l'écran ;

- les supprimer de l'écran ;

- interagir avec un champ de l'écran (augmentation du score, suppression d'une vie).

III - Résultat attendu : à quoi ressemblera notre gâteau

En tant que développeur, on a souvent tendance à se jeter à l'eau trop tôt : il ne faut pas confondre vitesse et précipitation.

Bien que l'application que nous allons créer ici soit très simple, il faut prendre de bonnes habitudes dès le départ.

Premièrement, nous allons faire un schéma de l'application finale :

Une partie pour afficher les vies, une autre le score, puis les ennemis qui défilent verticalement et enfin une zone faisant perdre les vies (quand les ennemis arrivent en bas de l'écran).

Histoire de vous mettre l'eau à la bouche et afin de vous motiver pour lire la suite, voilà à quoi ressemblera le résultat final (et ceci très facilement vous verrez).

IV - Présentation de nos ustensiles de cuisine

Pour cuisiner, il nous faut des instruments particuliers « simples », mais nous avons aussi la possibilité d'utiliser des accessoires plus pratiques et plus confortables, qui nous permettront d'être plus productifs et efficaces.

C'est la même chose avec Qt : vous pouvez tout « coder à la main » ou profiter des différents accessoires, bibliothèques et composants pour gagner en efficacité. Nous allons utiliser quelques-uns de ces éléments pratiques dans la suite.

IV-A - StackView

Ce composant Qt permet d'utiliser une « pile de navigation », comprenez par là un moyen de gérer vos pages de navigation. L'idée de passer par un système de pile permet, comme pour les tableaux, d'ajouter/supprimer/récupérer un élément. Seul l'élément tout en haut de la pile sera visible pour l'utilisateur.

IV-B - Modèles et ListModel

Le framework permet de gérer des sources de données sur lesquelles peuvent s'appuyer d'autres composants. Imaginez que vous puissiez créer et faire vivre une liste d'éléments, un dictionnaire… et y « connecter » un élément qui prendra en compte ces évolutions. Par exemple, vous pouvez avoir une liste d'ennemis (un « modèle ») et les afficher à l'écran sans écrire la moindre boucle.

Nous allons le voir dans le prochain chapitre.

IV-C - Repeater

Comme nous l'avons vu précédemment, on peut connecter un « modèle » avec un autre composant, c'est ici le but avec ce type de composant « Repeater ». Grâce à lui, vous pouvez indiquer qu'un élément ou groupe d'éléments seront « répétés » selon cette source de données.

Par exemple, imaginez une source de données constituée d'une suite de légumes, vous pouvez créer un « Repeater » englobant des rectangles avec du texte basé sur cette liste : vous aurez à l'écran une liste de rectangle avec ces noms de légumes à l'écran.

IV-D - Timer

Le composant « Timer » vous permet de gérer à la fois une action décalée (appeler une méthode au bout de N secondes) ou récurrente (ajouter des ennemis au jeu toutes les N secondes).

IV-E - AnimatedSprite

Une animation est une scène décomposée en une succession d'images qui s'enchaînent à un rythme assez rapide pour nous faire croire à un mouvement fluide. Le composant « AnimatedSprite » permet de créer une telle animation en paramétrant à partir d'une image les éléments qui formeront l'animation.

V - Préparation à cuisiner

V-A - Création d'une application Android dans Qt

Ouvrez Qt Creator et créez une application, modèle Android : Qt Quick 2.

Puis la version minimale de Qt et les cibles de compilation. Je sélectionne les trois cibles proposées, mais seules les cibles Android ARM et Desktop sont nécessaires.

La cible « Desktop » nous servira à vérifier notre jeu au cours du développement et la cible Android ARM sert logiquement à compiler notre application pour Android en fin de projet.

Après avoir validé toutes les étapes, vous vous retrouvez dans votre EDI avec le projet initié mais vide, comme ceci :

V-B - Avant de commencer

V-B-1 - Réflexion et organisation avant l'action

Comme je l'ai dit précédemment : ne nous précipitons pas. Souvent, on se lance dans le développement d'un jeu et c'est à la fin que l'on se pose ces questions toutes bêtes : « ah oui, comment implémenter l'écran de démarrage, le menu, etc. ? »

V-B-2 - Un jeu adapté à tous les écrans

Dans le développement mobile, la question se pose sur la taille de l'écran : en effet, votre jeu pourra autant être joué sur une tablette 9 pouces qu'un smartphone d'à peine 5 pouces, mais pour autant l'expérience et le « gameplay » devront rester les mêmes.

Par exemple, notre jeu en mode portrait sur une tablette donnée aurait des pluies de trois ou quatre ennemis, occupant toute la largeur de l'écran. Si le joueur utilise une grande tablette, les mêmes ennemis, chacun ayant à la même taille en pixels, n'occuperait plus que le tiers de l'écran. La situation serait pire pour un smartphone, qui, avec un écran plus étroit encore, n'afficherait qu'un ou deux ennemis…

Vous comprenez mieux le souci : il faudrait penser le code du jeu en fonction de plusieurs tailles d'écran. Notre approche permet d'imaginer le jeu pour une résolution donnée et le code adaptera les dimensions des différents éléments (ennemis, nuage, score…) à l'écran en respectant les proportions.

Pour cela, nous utiliserons le principe suivant dans cet article et cette application : nous partirons d'une résolution de base (720 × 960) et nous adapterons/convertirons toutes les dimensions des éléments à l'écran.

V-C - Installation de la base de notre application

V-C-1 - Préfixes

Pour Qt Creator, un préfixe est en quelque sorte un répertoire virtuel permettant de mieux organiser vos fichiers dans l'arborescence du projet. J'ai bien dit « virtuel », car l'emplacement réel de vos fichiers peut être différent de leur emplacement virtuel. Cela permet également une indépendance de l'implémentation physique (qui peut varier entre un smartphone Android, un ordinateur sous Windows ou macOS…)

J'ai pris ici l'initiative ici d'ajouter ces « préfixes » pour mieux ordonner notre projet.

Voici la liste des préfixes créés pour notre projet :

- « images » qui contiendra les images du jeu ;

- « js » qui contiendra les fichiers JavaScript ;

- « pages » qui contiendra les fichiers QML des pages ;

- « items » qui contiendra les fichiers QML des éléments du jeu (ennemis, effets spéciaux…).

V-C-2 - Modification du fichier QML principal

Éditez le fichier main.qml comme suit :

import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import "/js/Game.js" as Game
Window {
id: main
visible: true
color: "#224422"
property var oGame
contentOrientation :Qt.PortraitOrientation

function initApplication(){
this.oGame=Game;
//oGame.start(Screen.width,Screen.height);
this.oGame.start(300,750);
}
Component.onCompleted: initApplication()
}

Ce code recèle plusieurs choses intéressantes :

- l'identifiant « main » et sa propriété « oGame » ;

- l'appel à l'initialisation de l'application ;

- la possibilité de lancer le jeu en forçant la résolution.

La propriété oGame, qu'il faut bien garder en tête permettra d'utiliser les méthodes du moteur de jeu.

Enfin, vous voyez que, en fin de chargement du composant, on appelle une méthode initApplication permettant d'initialiser la taille réelle de l'écran.

Vous noterez que j'ai volontairement mis en commentaire l'appel de la méthode start avec les « vraies » valeurs de l'écran : ici, on force à 300 × 750 pour bien vérifier que l'adaptation de la résolution du jeu fonctionne bien.

V-C-3 - Ajout du moteur du jeu : le fichier JavaScript

Comme vu dans le précédent article, pour ajouter des fichiers au projet (ici, le code source du jeu), effectuez un clic droit sur « Ressources » pour ajouter un fichier JavaScript Game.js. Ensuite, copiez-y ce code :

//dimensions réelles de l'écran
var _width;
var _height;
//dimensions de base de notre jeu
var _virtualWidth=720;
var _virtualHeight=960;
//ratio permettant d'adapter à la résolution de l'écran
var _iRatio;
function getWidth(){
return _width;
}
function getHeight(){
return _height;
}
//fonction de démarrage du jeu
function start(width_,height_){
if(width_> height_){
_width=height_*(_virtualWidth/_virtualHeight);
}else{
_width=width_;
}
_height=_width*(_virtualHeight/_virtualWidth);
_iRatio=_width/_virtualWidth;
main.width=_width;
main.height=_height;

}
//convertit une dimension à la taille de l'écran
function convert(size_){
return size_*_iRatio;
}

Pour avoir un jeu qui s'adaptera à toutes les résolutions sans pour autant modifier le « gameplay », nous avons besoin de connaître la différence de ratio entre notre résolution de base (720 x 960) et celle réelle de l'écran de l'utilisateur.

Ainsi on évite d'avoir trois ennemis sur un smartphone et 14 sur une tablette 10 pouces.

Initialisées via la méthode start, ces propriétés permettent de calculer un ratio, utilisé dans la méthode convert pour adapter les dimensions des éléments à l'écran.

V-C-4 - Ajout des pages

Créons maintenant nos pages, qui correspondent à chacune des étapes du joueur, depuis le lancement de l'application jusqu'à l'écran des scores.

Il faut créer un fichier QML pour chacune de nos pages :

- la page de démarrage « splashscreen » ;

- la page de menu du jeu « menu » ;

- la scène, qui contiendra les éléments de jeu « scene » ;

- enfin, la page de fin de jeu « gameOver ».

V-C-5 - Ajout des images pour les pages

Ces pages utiliseront des images, que vous pouvez ajouter au projet.

Pour cela, cliquez droit sur l'élément ressources, sélectionnez « Ajouter des éléments existants » et choisissez les images à ajouter au projet.

Même si les préfixes sont des dossiers virtuels, ils seront utilisés pour indiquer les chemins des éléments dans le code. Ainsi, l'image « pageGameOver.png » sera bien accessible via « /images/pageGameOver.png » (même si tous les éléments sont à la racine physique du projet).

V-D - Ajout de la navigation générale

V-D-1 - Édition des écrans de navigation

Notre première étape est de coder les différentes pages du jeu, tout d'abord sans interaction entre elles.

Éditez votre fichier /pages/Splashcreen.qml, qui est la page de démarrage du jeu. Elle contient votre logo d'éditeur d'applications et disparaît au profit de la page « Menu » au bout d'un certain temps.

import QtQuick 2.0
Rectangle{
visible:true
color:'#d95e22'
width:main.oGame.getWidth()
height:main.oGame.getHeight()
Image{
width:main.oGame.convert(237)
height:main.oGame.convert(105)
source:"/images/pageLogoMobileDupot.jpg"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
smooth: true
}
Timer {
id:timerSplashscreen
interval: 4000;
running: true
repeat: false
onTriggered: main.oGame.gotoMenu();
}
}

Passons au menu maintenant, appelé automatiquement après le « Splashscreen ».

Éditez le fichier pour créer notre menu de démarrage /pages/Menu.qml.

import QtQuick 2.0
import QtQuick.Controls 1.5
Rectangle {
visible:true
color: "#1868b2"
width:main.oGame.getWidth()
height:main.oGame.getHeight()
Image{
source:"/images/pageStartGame.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width:main.oGame.convert(400)
height:main.oGame.convert(650)
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("Jouer!")
onClicked:main.oGame.gotoScene()
}
}
}

Le bouton permet d'aller sur la « Scene », éditez le fichier /pages/Scene.qml.

import QtQuick 2.0
import QtQuick.Controls 1.5
Rectangle{
visible:true
color:'#1868b2'
width:main.oGame.getWidth()
height:main.oGame.getHeight()
Button {
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("go gameover!")
onClicked:main.oGame.gotoGameover()
}
}

Pour le moment, cette page contient un simple bouton permettant de vérifier le bon fonctionnement de la navigation en lançant la page de fin de jeu.

Éditez le fichier /pages/GameOver.qml.

import QtQuick 2.0
import QtQuick.Controls 1.5
Rectangle {
visible:true
color: "#465973"
width:main.oGame.getWidth()
height:main.oGame.getHeight()
Image{
source:"/images/pageGameOver.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width:main.oGame.convert(400)
height:main.oGame.convert(650)
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("Re-jouer!")
onClicked:main.oGame.gotoScene()
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 50
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("Retour au menu")
onClicked:main.oGame.gotoMenu()
}
}
}

V-D-2 - Ajout de la mécanique de changement de pages

Une fois nos pages codées, il est temps d'écrire la partie qui permet de passer de l'une à l'autre.

Éditez le fichier main.qml pour ajouter l'objet StackView et les deux méthodes permettant d'afficher/fermer une page.

//le composant qui nous permettra de naviguer dans le jeu
StackView {
id: stack
width: parent.width
height:parent.height
}
function popPage(){
stack.pop();
}
function launchPage(sView){
return stack.push('qrc:/pages/'+sView+'.qml');
}

Vous voyez ici les fonctions pour afficher une page (launchPage) et pour la fermer (popPage).

Ensuite, éditez le fichier Game.js pour faire le lien entre le composant QML et le code JavaScript en ajoutant les méthodes permettant d'afficher ces pages.

var _oPageScene;
//fonctions de navigation
function gotoSplashscreen(){
main.launchPage('Splashscreen');
}
function gotoMenu(){
main.launchPage('Menu');
}
function gotoScene(){
_oPageScene=main.launchPage('Scene');
}
function gotoGameover(){
main.launchPage('GameOver');
}

Comme vous le voyez, on peut facilement faire le lien entre les deux éléments : ici, à partir du code JavaScript, on appelle la méthode launchPage déclarée dans le composant QML main.qml.

Vous remarquerez que, lors du changement de page pour afficher la scène, on stocke le retour de la méthode de lancement de page, ceci afin de permettre par la suite d'interagir avec cet écran.

Nous en profitons pour ajouter l'appel à cette page splashscreen dès la fonction start de notre fichier JavaScript ainsi :

//fonction de démarrage du jeu
function start(width_,height_){
if(width_> height_){
_width=height_*(_virtualWidth/_virtualHeight);
}else{
_width=width_;
}
_height=_width*(_virtualHeight/_virtualWidth);
_iRatio=_width/_virtualWidth;
main.width=_width;
main.height=_height;
//appel à la page splashscreen
gotoSplashscreen();
}

V-E - Un petit aperçu dès le début de ce projet

Vous pouvez à ce stade déjà compiler pour voir le résultat :

Vous pouvez modifier les valeurs pour voir le résultat évoluer :

function initApplication(){
this.oGame=Game;
//oGame.start(Screen.width,Screen.height);
this.oGame.start(300,750);
}

Pour une résolution basse :

Pour une résolution moyenne :

Comme vous le voyez, la taille du logo respecte les proportions, peu importe la taille de la fenêtre.

VI - À vos fourneaux

VI-A - Ajout d'une pluie d'ennemis

VI-A-1 - Préparation

Comme nous l'avons vu précédemment, nous allons utiliser ici un « Repeater » couplé à un « ListModel » : le modèle contiendra les ennemis affichés à l'écran, tandis que le composant « Repeater » se chargera de les afficher.

Modifiez votre fichier main.qml pour ajouter votre liste d'ennemis.

ListModel{
id:modelEnemies
}

Il est vide ! Nous l'alimenterons plus loin dans le fichier JavaScript.

Passons à notre pluie d'ennemis : créez un fichier items/Enemy.qml et écrivez-y :

import QtQuick 2.0
Repeater{
model:modelEnemies
Rectangle{
width:main.oGame.convert(100)
height:main.oGame.convert(100)
color:"#770000"
x:model.x
y:model.y
PropertyAnimation on y {
to:main.oGame.getHeight()
duration: 5000
onStopped: die();
}
}
function die(){
modelEnemies.remove(model.index);
}
}

Quelques explications : premièrement, vous voyez un composant « Repeater » (vu plus haut) connecté à notre « ListModel » modelEnemies. Ce « Repeater » contient un rectangle rouge dont les coordonnées sont des propriétés de « model ».

En effet, chaque « répétition » de notre rectangle utilisera comme cordonnées les propriétés fournies dans le modèle des ennemis.

Ensuite, on voit un composant « PropertyAnimation », qui permet d'ajouter une animation sur ce rectangle : on lui indique ainsi de modifier l'ordonnée de notre élément jusqu'à la hauteur de l'écran pour donner l'impression de chute.

Nous indiquons que, à la fin de cette animation, il faut appeler la méthode die() (pour supprimer cet ennemi).

Il nous faut ajouter ce composant « Enemy » sur notre scène. Éditez le fichier /pages/Scene.qml pour y ajouter ce composant fraîchement écrit.

Utilisant ici des préfixes, il vous faut ajouter une phase d'import pour indiquer que ce composant est enregistré dans « /items » et non au même endroit que votre page « scene ».

Ceci avec le code suivant : import "qrc:/items/"

VI-A-2 - Intégration du composant Enemy à notre application

Vous devez simplement ajouter le nom du composant, comme si c'était un composant natif ; ici, pour Enemy.qml, il suffira d'ajouter :

Enemy{}

Voici à quoi ressemble votre page avec ces deux ajouts (imports et Enemy) :

import QtQuick 2.0
import QtQuick.Controls 1.5
import "qrc:/items/"
Rectangle{
visible:true
color:'#1868b2'
width:main.oGame.getWidth()
height:main.oGame.getHeight()
Button {
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("go gameover!")
onClicked:main.oGame.gotoGameover()
}
Enemy{}
}

VI-A-3 - Permettre l'appel d'ajout d'ennemis à l'écran

Maintenant, retournons à notre fichier JavaScript pour écrire le code permettant d'ajouter des ennemis.

Éditez /js/Games.js pour ajouter la fonction addEnemy, qui permettra d'ajouter des ennemis à l'écran en ajoutant un élément au modèle.

function addEnemy(){
modelEnemies.append({"x":convert(20),"y":0});
}

Si vous ajoutez un bouton sur notre scène appelant cette fonction, vous pourrez tester que, en appuyant dessus, un nouvel ennemi apparaîtra et tombera.

Vous pouvez le faire en ajoutant le code suivant sur le fichier /pages/Scene.qml :

Button {
y:main.oGame.convert(200)
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("ajouter enemie")
onClicked:main.oGame.addEnemy()
}

Ce qui nous intéresse, c'est de voir régulièrement un ennemi s'ajouter à l'écran. Pour cela, nous allons utiliser le composant « Timer ».

Il vous faut modifier ce même fichier de scène pour y ajouter ce « Timer » via le code :

Timer {
id:timerEnemy
interval: 1000;
running: true
repeat: true
onTriggered: main.oGame.addEnemy();
}

Détaillons un peu ce code :

- « id » : l'identifiant qui permettra d'arrêter ce « Timer » en cas de fin de jeu ;

- « interval » : l'intervalle entre chaque itération ;

- « running » : indique si le « Timer » est actif ;

- « onTriggered » : le code à appeler à chaque fois ; ici, notre fonction d'ajout d'ennemi.

VI-B - Amélioration de cette pluie pour qu'elle arrose bien tout l'écran

Si vous testez le code actuellement, vous aurez bien régulièrement un nouveau carré rouge qui apparaît à l'écran et tombe. Le problème, c'est que son apparition se fait toujours au même endroit.

Ce qui nous intéresse, dans un jeu de ce type, c'est de voir des ennemis apparaître un peu partout à l'écran, voire de manière aléatoire, pour ajouter du piment à la jouabilité.

L'idée est de faire varier l'abscisse et le nombre des ennemis. Pour ce faire, ajoutez d'abord une variable globale _xEnemy qui permettra d'afficher chaque ennemi un peu plus à droite (jusqu'à la limite de l'écran).

var _xEnemy=0;

Ensuite, faites varier la coordonnée x de chaque ennemi à chaque itération de notre boucle d'ajout d'ennemi.

function addEnemy(){
modelEnemies.append({x:_xEnemy,y:0});
_xEnemy+=convert(120);
if(_xEnemy > _width){
_xEnemy=0;
}
}

Ici l'ajout d'ennemi est linéaire, mais vous pouvez écrire un mode d'ajout aléatoire pour améliorer l'expérience de jeu.

VI-C - Ajout d'interactions sur nos ennemis

Pour que cette application soit vraiment un jeu, il faut que, lorsque l'on clique sur un ennemi, il disparaisse.

À cette fin, ajoutez dans notre composant d'ennemi un composant « MouseArea », qui servira à récupérer les interactions de l'utilisateur.

MouseArea{
anchors.fill: parent
onClicked:killedByPlayer()
}

Ajoutez aussi une fonction de suppression de l'ennemi par l'utilisateur, appelée chaque fois que le joueur clique sur un ennemi.

function killedByPlayer(){
modelEnemies.remove(model.index);
}

Ce qui donne au final sur /items/Enemy.qml :

import QtQuick 2.0
Repeater{
model:modelEnemies
Rectangle{
width:main.oGame.convert(100)
height:main.oGame.convert(100)
color:"#770000"
x:model.x
y:model.y
PropertyAnimation on y {
to:main.oGame.getHeight()
duration: 5000
onStopped: die();
}
MouseArea{
anchors.fill: parent
onClicked:killedByPlayer()
}

function die(){
modelEnemies.remove(model.index);
}
function killedByPlayer(){
modelEnemies.remove(model.index);
}
}
}

Vous noterez que les deux actions (la chute en bas de l'écran et le clic utilisateur) appellent deux fonctions différentes, qui font la même chose pour le moment. Cela permettra de différencier, par la suite, l'ajout de points au score ou la diminution du nombre de vies.

VI-D - Ajout d'un module score et vies

VI-D-1 - Création du composant de score

Commençons par le score. Ajoutons un nouveau composant /items/Score.qml avec le code suivant :

import QtQuick 2.0
Item{
anchors.right: parent.right
Text{
id:"txt"
text:'000000'
anchors.right: parent.right
font.pixelSize: main.oGame.convert( 30)
font.bold: true
color:"white"
}
function setText(sText_){
sText_='0000000'+sText_;
txt.text=sText_.substr(-6);
}
}

Deux choses ici :

- un composant « Text » qui permet d'afficher le score aligné à droite ;

- une fonction « setText » qui permet de mettre à jour ce score en conservant les « 0 ».

VI-D-2 - Inclusion dans la scène

Comme nous l'avons fait précédemment pour le composant « Enemy » en incluant le nom du composant fraîchement développé, on fait de même pour le score. Ajoutons le code suivant dans notre page /pages/Scene.qml :

Score{
id:"oScore"
}

Puis, afin de mettre à jour le texte du score, ajoutons une fonction :

function setScore(sText_){
oScore.setText(sText_);
}

VI-D-3 - Ajout dans le fichier JavaScript du lien vers le score

Dans un premier temps, il faut ajouter une variable qui contiendra ce score, puis une méthode pour le mettre à jour à l'écran.

Ajoutez une propriété du score à votre fichier JavaScript /js/Game.js :

var _iScore=0;

Puis la méthode pour le mettre à jour :

function scoreUp(){
_iScore++;
_oPageScene.setScore(_iScore);
}

La fonction permet ici d'incrémenter notre score puis de le mettre à jour à l'écran.

VI-D-4 - Implémentation du mécanisme des vies

La démarche reste la même que pour le score, à la différence que, pour nos vies, nous n'afficherons pas dans un champ texte, mais plutôt une succession d'éléments (des cœurs ou toute icône de votre choix…).

Comme pour le score, commençons par créer un composant /items/Lifes.qmlpour afficher nos vies restantes :

import QtQuick 2.0
Repeater{
model:4
delegate: Rectangle {
visible:true
width: main.oGame.convert(80)
height: main.oGame.convert(80)
color:"#ff0000"
x:main.oGame.convert(80+5)*model.index
y:0
}
function setLifes(nb_){
model=nb_;
}
}

Ici, vous voyez une autre utilisation du composant « Repeater » : on indexe cette fois la répétition de rectangle à un simple nombre (un modèle comme un autre, dans le cas où chaque élément n'a pas plus de données qu'un nombre) et l'on permet via la fonction setLifes de changer le nombre de répétitions.

VI-D-5 - Inclusion sur la scène

Même chose que pour le score, en ajoutant cette fois le code suivant à votre page de scène :

Lifes{
id:"oLife"
}

Et la fonction associée :

function setLifes(iNb_){
oLife.setLifes(iNb_);
}

VI-D-6 - Ajout du lien dans le fichier JavaScript vers ce composant de gestion de vies

Ajoutez comme pour le score, une variable contenant le nombre de vies à votre fichier JavaScript /js/Game.js :

var _iLife=4;

Et la fonction associée :

function lifeDown(){
_iLife--;
_oPageScene.setLifes(_iLife);
if(_iLife < 0){
_oPageScene.stopTimer();
modelEnemies.clear();
_xEnemy=0;
_iLife=4;
_iScore=0;
_oPageScene.setLifes(_iLife);
gotoGameover();
}
}

On profite pour vérifier le nombre de vies restantes et ainsi renvoyer à la page de fin de jeu lorsque le joueur n'a plus de vie.

Vous lisez également que l'on réinitialise les variables du jeu afin de préparer la prochaine partie (quatre vies, un score à zéro…).

Vous noterez l'appel pour stopper le « Timer » que vous pouvez ajouter ainsi dans votre fichier /pages/Scene.qml :

function stopTimer(){
timerEnemy.stop();
}

VI-E - Ajout du lien entre les ennemis et nos modules score et vies

Maintenant, il nous faut lier les actions de l'utilisateur à ces deux fonctions (mise à jour du score et du nombre de vies). Modifiez les deux fonctions de votre fichier /items/Enemy.qml ainsi :

function die(){
main.oGame.lifeDown();
modelEnemies.remove(model.index);
}
function killedByPlayer(){
main.oGame.scoreUp();
modelEnemies.remove(model.index);
}

Vous voyez ici un simple appel à nos deux fonctions précédemment écrites après avoir retiré les ennemis de l'objet « model ».

VII - Finitions

À ce stade du tutoriel, vous avez un jeu fonctionnel. Nous allons maintenant voir comment l'embellir.

VII-A - Ajout d'une animation sur les ennemis : découverte des sprites

Des carrés rouges, c'est bien, mais des ennemis animés, c'est mieux : nous allons ici ajouter des « sprites » animés. L'animation représente les mouvements de l'aile de l'ennemi pour donner l'impression de vol.

Ajoutez une image « BadSprite.png » à votre projet :

Modifiez votre fichier QML pour passer le rectangle rouge en « transparent » et ajouter un composant d'animation dans le composant « Rectangle »:

AnimatedSprite {
width:parent.width
height:parent.height
anchors.centerIn: parent
source: "/images/BadSprite.png"
frameCount: 3
frameRate: 1/4
frameSync: true
frameWidth:  80
frameHeight: 80
}

Votre image de 240 px sur 80 px sera découpée par l'objet d'animation de sprite : chaque étape correspond à une sous-image, un « sprite » de 80 px de largeur sur 80 px de hauteur ; toutes les étapes sont mises les unes à la suite des autres.

VII-B - Ajout d'explosions à la suppression des ennemis

Nous allons ajouter des animations d'explosion lors du clic sur un ennemi.

Ajoutons d'abord un objet « model » dans notre fichier main.qml :

ListModel{
id:modelBoom
}

Puis, créons un nouveau composant QML /items/Boom.qml avec le code suivant :

import QtQuick 2.0
Repeater{
model:modelBoom
delegate: Rectangle {
width:main.oGame.convert(80)
height:main.oGame.convert(80)
color: "transparent"
x:model.x
y:model.y
AnimatedSprite {
width:parent.width
height:parent.height
anchors.centerIn: parent
source: "/images/BoomSprite.png"
frameCount: 3
frameRate: 1/2
frameSync: true
frameWidth:  80
frameHeight: 80
loops: 3
onRunningChanged:{
if (!running) {
die();
}
}
}
function die(){

modelBoom.remove(index);
}

}
}

Avec ce couple « composant QML/Repeater », on peut ajouter des explosions à l'écran : chaque nouvelle explosion correspondra à une entrée ajoutée à cet objet « modelBoom », en précisant les coordonnées de l'ennemi éliminé.

Pour cela, nous allons modifier la fonction de suppression d'un ennemi en modifiant ainsi le composant /items/Enemy.qml :

function killedByPlayer(){
main.oGame.scoreUp(x,y);
modelEnemies.remove(model.index);
}

Notez ici une légère différence sur l'appel de scoreUp  : on lui passe désormais les coordonnées x,y de l'ennemi avant de le supprimer de l'écran afin d'avoir les bonnes informations pour afficher l'explosion.

Cela implique bien entendu de modifier également la fonction en question dans le fichier JavaScript /js/Game.js :

function scoreUp(x_,y_){
modelBoom.append({x:x_,y:y_});
_iScore++;
_oPageScene.setScore(_iScore);
console.debug('scoreUp'+_iScore);
}

On récupère les coordonnées de l'ennemi pour ajouter au modèle des explosions aux coordonnées de l'ennemi.

Enfin, pour que la mayonnaise prenne, il ne faut pas oublier d'ajouter ce composant « Boom » dans votre page « Scene ».

Éditez donc le fichier /pages/Scene.qml pour ajouter, en une ligne, ce composant d'explosion (aux côtés du composant d'ennemi) :

Enemy{}
Boom{}

VII-C - Amélioration du score : effet graphique

Qt dispose de bien des outils qui peuvent vous faciliter la vie, dont des effets graphiques comme des ombres.

Vous pouvez ici très simplement ajouter une ombre à votre score pour le mettre en valeur. Pour cela, modifiez votre fichier /items/Score.qml.

Commencez par ajouter l'import de la bibliothèque d'effets graphiques via :

import QtGraphicalEffects 1.0

Puis modifiez le code existant pour utiliser ces effets :

Text{
id:"txt"
text:'000000'
anchors.right: parent.right
font.pixelSize: main.oGame.convert( 30)
font.bold: true
color:"white"
}
DropShadow {
anchors.fill: txt
horizontalOffset: 3
verticalOffset: 3
radius: 1.0
samples: 8
color: "#80000000"
source: txt
}

VII-D - Amélioration de nos boutons

Nous avons ici créé plusieurs composants personnalisés pour les utiliser dans notre jeu, mais nous pouvons faire de même pour des éléments plus « simples » comme les boutons. Bien évidemment, Qt Quick propose des fonctionnalités pour personnaliser les boutons, mais il est aussi simple de coder soi-même un bouton adapté à nos besoins.

Par exemple ici, créez un composant bouton /items/Bouton.qml :

import QtQuick 2.4
import QtGraphicalEffects 1.0
Item {
x:0
y:0
property int _width;
property int _height;
property string _text;
property var _link;
width:_width
height:_height
Rectangle {
id: rectangle1
color: "#d95e22"
radius: 3
x:0
y:0
width:_width
height:_height
Text {
anchors.centerIn: parent
text: qsTr(_text)
color:"#ffffff"
font.pixelSize: main.oGame.convert( 20)
}
MouseArea{
anchors.fill: parent
onClicked:_link()
}
}
DropShadow {
anchors.fill: rectangle1
horizontalOffset: 3
verticalOffset: 3
radius: 1.0
samples: 8
color: "#80000000"
source: rectangle1
}
}

Pas besoin de tout détailler, notez juste un point important : les propriétés _width, _height, _text et _link, qui permettront au sein des pages de personnaliser ces boutons.

Prenons pour exemple l'implémentation dans le menu, on remplacera le code du bouton simple :

Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: main.oGame.convert(291)
height: main.oGame.convert(33)
text: qsTr("Jouer!")
onClicked:main.oGame.gotoScene()
}

Par :

Bouton{
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
_width: main.oGame.convert(291)
_height: main.oGame.convert(33)
_text: qsTr("Jouer!")
_link:main.oGame.gotoScene
}

Vous voyez ici la « légère » différence d'implémentation entre un composant bouton « normal » et notre version personnalisée.

VII-E - Un peu de profondeur avec des nuages en fond

Avec les éléments vus dans ce tutoriel, vous pouvez facilement ajouter un couple Repeater/composant QML pour faire défiler des nuages afin d'ajouter un peu de profondeur au jeu.

VIII - Conclusion

Vous avez pu voir que Qt et ses outils rendaient la vie plus facile pour écrire des jeux Android.

Plutôt que de réinventer la roue ou vous poser des questions de mécaniques, vous pouvez simplement réfléchir à votre « game play » et profiter de Qt pour le réaliser facilement.

Vous pouvez retrouver le code complet de l'application sur GitHub.

IX - Remerciements

Je souhaiterais remercier dourouc05 et LittleWhite pour leur aide précieuse et leur patience. L'écriture de cet article m'aura beaucoup appris sur ce formidable framework puissant et accessible. Je remercie également f-leb pour ses corrections orthographiques.