Développer un jeu multijoueur Android avec Qt, C++ et QML

Créer des jeux multijoueurs avec les websockets

Nous allons apprendre comment implémenter une communication websocket en mêlant à la fois du C++, du QML et du JavaScript. 1 commentaire Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Vous avez pu voir dans les précédents articles (Créer un shoot them up partie 1 et 2) à quel point Qt pouvait être une bibliothèque pratique et confortable pour développer des applications Android.

Depuis, j'ai pu développer plusieurs applications dont la dernière : un « bomberman-like » multijoueur qui se joue en réseau local (LAN). J'ai ainsi remarqué les difficultés à trouver des informations claires et exactes pour implémenter les fonctionnalités réseau.

Cet article aura donc pour but de partager mon expérience et également d'appréhender des points supplémentaires comme la communication entre le C++ et QML/JavaScript.

J'utiliserai ici le code source de ce dernier jeu pour appuyer mes explications. Nous ne développerons pas le jeu de A à Z comme précédemment : nous aborderons des thèmes/problématiques précis avec du code en exemple

L'ensemble de la solution étant disponible sur GitHub ainsi que sur le Play Store Android.

Image non disponible

Avant de commencer, voici les sujets que nous allons aborder :

  • présentation générale de l'application ;
  • problématique d'une gestion multijoueur ;
  • présentation de la technologie choisie : les websockets ;
  • problématique du QML et choix de l'utilisation du C++ en complément ;
  • communication C++ / QML-JavaScript ;
  • exemples d'implémentations dans le jeu ;
  • trucs et astuces et autres questions/réponses utiles.

II. Présentation générale de l'application

L'application dont il est question ici est un bomberman-like nommée « Bomby ».

Pour ceux qui ne connaissent pas, le but est de supprimer ses adversaires en utilisant des bombes. Les joueurs peuvent se déplacer horizontalement et verticalement, la carte contient des obstacles destructibles et d'autres non. Ici nous voulons ajouter de l'interactivité avec un mode de jeu multijoueur en réseau local.

L'arborescence du projet est la suivante :

Image non disponible

Vous pouvez remarquer trois types de fichiers :

  • les fichiers C++ ;
  • les fichiers QML (pour les pages et les objets) ;
  • les images.

Dans les précédents articles, nous expliquions que Qt permettait de développer en JavaScript et QML : pour cela, nous utilisions un seul fichier C++ donc l'unique but et d'initier l'application.

Nous verrons plus loin pourquoi il y en a plus cette fois-ci (surtout utilisés pour la communication des websockets).

II-A. Problématiques d'une gestion multijoueur

Créer un jeu multijoueur sous-entend plusieurs choses :

  • il faut une relation client/serveur ;
  • chaque joueur doit envoyer ses actions au joueur « serveur » afin de diffuser à l'ensemble le statut de la partie ;
  • un protocole doit être trouvé pour permettre ces échanges.

II-B. Websockets

La problématique était de permettre de jouer une partie à plusieurs, mais en essayant d'éviter d'avoir besoin d'un serveur.

Il y avait donc plusieurs choix possibles :

  • le Bluetooth ;
  • le wi-fi.

Après plusieurs hésitations, je me suis tourné vers le wi-fi.

Les avantages par rapport au Bluetooth :

  • périmètre connu (TCP/IP…) ;
  • possibilité de jouer avec des ordinateurs n'incluant pas toujours le Bluetooth ;
  • possibilité de créer un client web (sans installation) ;
  • possibilité de jouer sans accès internet également (comme le Bluetooth) ;
  • expérience personnelle négative du Bluetooth et de ses aléas.

Une fois le canal de communication choisi (TCP/IP en wi-fi/Ethernet), il fallait choisir le protocole : socket ou websocket. Je me suis dit que les websocket permettaient une compatibilité plus aisée.

II-C. Problématique du QML, pourquoi avoir choisi le C++

Au départ, j'étais parti sur le composant QML WebSocketClient, vous pouvez voir la version de l'application avant cette décision sur mon dépôt GitHub.

Il s'initialisait en quelques lignes et semblait fonctionner facilement. Tout ceci fonctionnait très bien tant que l'on avait l'adresse du serveur et du client écrite en dur dans le QML.

Le souci est que l'on souhaite créer des parties à la volée et ainsi permettre à plusieurs personnes de créer une partie en utilisant un de leur smartphone/tablette comme serveur.

Ceci induit donc qu'il faut pouvoir modifier le port utilisé pour le joueur hébergeant la partie (serveur), mais également des joueurs (clients) qui s'y connecteront.

Et là, le composant QML a atteint ses limites. J'ai essayé beaucoup de solutions pour permettre de se déconnecter/reconnecter à une autre IP en vain.

Le choix s'est donc porté sur l'utilisation de la bibliothèque C++ plutôt que du composant QML ; de plus, c'était l'occasion de voir comment mixer du Qt/C++ et QML.

II-D. Communication entre C++, QML et JavaScript

En développant cette application, j'ai appris à mes dépens qu'il était difficile de trouver les bonnes informations pour faire communiquer le C++, le QML et le JavaScript.

Nous allons donc en profiter ici pour expliquer en un seul article ces différents ponts de communication, mais vous verrez qu'il est très pratique de pouvoir utiliser les avantages de chaque technologie.

II-D-1. Communication C++ / QML

Dans un premier temps, nous allons permettre une communication entre notre classe C++ et nos fichiers QML.

Commençons par créer un accès au code compilé par notre couche QML.

Pour cela, nous créons en C++ une classe dont certaines méthodes seront accessibles en QML.

Voici la classe en question qui sera accessible :

 
Sélectionnez
class ApplicationData : public QObject
{
    Q_OBJECT
public:

    Server *oServer;
    Client *oClient;

    bool bServer=false;

    QObject *oQml;

    void setQmlObject(QObject *oQml_){
        oQml=oQml_;
    }

    Q_INVOKABLE QString getIp() const {

        QStringList addresses;
          foreach(QHostAddress address, QNetworkInterface::allAddresses())
          {
            // Filtre les adresses locales ...
            if(address != QHostAddress::LocalHostIPv6
               && address != QHostAddress::LocalHost
               && !address.isInSubnet(QHostAddress::parseSubnet("169.254.0.0/16"))
               && !address.isInSubnet(QHostAddress::parseSubnet("FE80::/64")))
            {
              addresses << address.toString();
            }
          }

        return addresses[0];
    }


};

Ceci est un extrait de la classe, il n'est pas utile ici de lister la totalité de son contenu.

Ensuite, dans votre fichier C++ principal, vous instanciez et passez en propriété contextuelle une instance de cette nouvelle classe :

 
Sélectionnez
int main(int argc, char *argv[]){
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    ApplicationData data;

    engine.rootContext()->setContextProperty("applicationData", &data);
    engine.load(QUrl(QStringLiteral("qrc:///main.qml")));


    QList<QObject*> rootObjt = engine.rootObjects();

    data.setQmlObject( rootObjt[0] );

    return app.exec();
}

Vous pourrez l'utiliser simplement avec le mot clé applicationData (nom donné à la propriété contextuelle côté C++).

Par exemple côté QML, on appelle applicationData et la méthode désirée, tout simplement :

 
Sélectionnez
1.
2.
3.
4.
5.
function initApplication(){

    console.log('mon ip' + applicationData.getIp());
        
}

Revenons à notre fichier C++ principal et détaillons certaines parties du code.

Instanciation d'un objet « moteur » de rendu QML, qui nous charge le fichier QML principal, puis l'affiche à l'écran :

 
Sélectionnez
1.
QQmlApplicationEngine engine ;

Création d'une instance de notre objet qui sera partagée avec le code QML :

 
Sélectionnez
ApplicationData data;

Utilisation de notre moteur pour créer une propriété de contexte applicationData avec notre classe spéciale :

 
Sélectionnez
engine.rootContext()->setContextProperty("applicationData", &data);

Et enfin le code qui suit permet de faire l'inverse, c'est-à-dire récupérer une référence de notre fichier QML utilisable dans notre code C++ :

 
Sélectionnez
QList<QObject*> rootObjt = engine.rootObjects();

data.setQmlObject( rootObjt[0] );

II-D-2. Communication QML/JavaScript

Dans les précédents articles, vous avez pu lire qu'un des avantages de Qt était d'écrire l'application dans des langages simples comme QML et JavaScript. Avec l'ajout de cette partie C++, vous pourriez penser qu'il faudrait alors renoncer à développer en JavaScript et vous replonger dans vos bouquins du langage compilé.

Il n'en est rien, vous pouvez continuer à concevoir la logique de votre jeu dans ce langage de script.

Voici comment simplement en JavaScript appeler le code C++ de la classe ApplicationData :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
function webSocketClient_send(message_){

    if(false === _bConnected){
        applicationData.connectClient(_urlWebsocket + ":" + _urlWebsocketPort);

    }else{
        var iTeamToSend = _tTeamInverse[_sTeam];
        if(_gameStarted){
            if(modelPerso.get(iTeamToSend).visible === true){
                applicationData.sendMessage(_sTeam + ":" + message_);
            }else{
                console.log('gameover can not play');
            }
        }else{
            applicationData.sendMessage(_sTeam + ":" + message_);
        }
    }
}

Cette fonction JavaScript sert à envoyer un message via websocket. Vous pouvez noter que l'on appelle naturellement applicationData, qui est notre « pont C++ » permettant d'appeler notre classe C++ gérant les websocket.

II-E. Exemples d'implémentation dans le jeu

Maintenant que vous avez connaissance de la technologie utilisée et des raisons, voyons son implémentation dans ce jeu.

L'idée est de permettre à plusieurs joueurs de se déplacer sur la carte et de déposer des bombes, ceci en réseau LAN.

II-E-1. Initialisation du serveur

Dans un premier temps le premier joueur initie la partie et se désigne comme « serveur » de la partie.

Dans l'interface, un bouton permet d'être serveur ou de se connecter à une partie : en cliquant dessus, on affiche un formulaire permettant de choisir le port qui sera utilisé puis via un bouton de confirmation, de lancer le serveur.

Dans ce fichier ServerSide.qml, voici le code du bouton en question :

 
Sélectionnez
Bouton{
    id:btnToServe
    x: main.oGame.convert(80)
    y: main.oGame.convert(136)
    _width: main.oGame.convert(530)
    _height: main.oGame.convert(60)
    _text: qsTr("Ecouter sur ce port !")
    _link:function(){applicationData.connectServer(port.text)}
}

Le lien du bouton appelle la méthode connectServer() de notre code C++ via la fameuse propriété de contexte applicationData.

Et voyons le code d'applicationData dans main.cpp :

 
Sélectionnez
class ApplicationData : public QObject
{
    Q_OBJECT
public:

Q_INVOKABLE void connectServer(QString port_){
        oServer=new Server(port_,true,oQml);
        bServer=true;
    }

Premièrement, vous noterez la signature particulière de la méthode de connexion avec le mot clé « Q_INVOKABLE » ; ensuite , on initialise un objet Server qui contient le code réel du serveur : en effet pour faire au plus propre, j'ai distingué une classe client et une classe serveur.

Avant de la présenter, signalons un détail sur cette classe en se basant sur son fichier d'en-tête server.h

Cette classe étend la classe QWebSocketServer :

 
Sélectionnez
#include <QtWebSockets/QWebSocket>

#include <QtWebSockets/QWebSocketServer>



class Server : public QWebSocketServer
{
    Q_OBJECT
public:
    Server();

    Server(QString port,bool bDebug,QObject * Qml);

Regardons le constructeur de cette classe server.cpp :

 
Sélectionnez
#include "server.h"
#include "QtWebSockets/QWebSocketServer"
#include "QtWebSockets/QWebSocket"
#include <QtCore>

QT_USE_NAMESPACE

Server::Server( QString port,bool bDebug, QObject * oQml_) : QWebSocketServer(QStringLiteral("Server"),
                                                             QWebSocketServer::NonSecureMode){
    _oQml=oQml_;
    //qDebug()<<"constructeur";

    _Team=new QStringList('blue','red','green','yellow');


    connect(this, &QWebSocketServer::newConnection,
            this, &Server::onNewConnection);

    connect(this,  &QWebSocketServer::serverError, this,&Server::onError);

    connect(this,  &QWebSocketServer::acceptError, this,&Server::acceptError);

    connect (this, &QWebSocketServer::closed, this, &Server::onClose);



    if (listen(QHostAddress::Any, port.toInt() ) ){
        //signal
        QMetaObject::invokeMethod(_oQml, "serverIsConnected");

    }else{
        //qDebug() << errorString();
    }
}

Comme dit précédemment, elle étend la classe QWebSocketServer, en appelant son constructeur, cela crée une instance serveur. On en profite pour « connecter » certains signaux des méthodes de la classe.

Intéressons-nous à deux lignes importantes, d'abord le début d'écoute de notre serveur :

 
Sélectionnez
 if (listen(QHostAddress::Any, port.toInt() ) ){
        //signal
        QMetaObject::invokeMethod(_oQml, "serverIsConnected");

À partir du moment où l'instance accepte d'écouter sur le port indiqué, on appelle la méthode serverIsConnected du fichier QML. Voyons ce que fait cette méthode sur main.qml :

 
Sélectionnez
function serverIsConnected(){
    stack.currentItem.serverIsConnected();
}

Celle-ci appelle la même méthode issue de la page chargée à cet instant.

L'application charge selon l'activité du joueur des écrans différents, l'idée est d'appeler la même méthode implémentée sur chacun des écrans de manière différente (adapté selon les contraintes d'affichage en question).

Zoomons sur l'écran QML vu précédemment ServerSide.qml :

 
Sélectionnez
function serverIsConnected(){
    rectangleUser.visible=true;
    btnToServe.visible=false;

    main.oGame._isServer=true;
}

Elle va :

  • afficher un bloc ;
  • désactiver le bouton pour démarrer le serveur (c'est déjà fait) ;
  • indiquer à l'instance JavaScript du jeu que celle-ci est l'hébergeur de la partie.

II-E-2. Connexion d'un client au serveur

Enfin, regardons cette deuxième ligne importante, appelée à chaque connexion :

 
Sélectionnez
connect(this, &QWebSocketServer::newConnection,
            this, &Server::onNewConnection);

Faisons un point sur cette méthode :

 
Sélectionnez
void Server::onNewConnection(){

    QWebSocket *pSocket = nextPendingConnection();

    connect(pSocket, &QWebSocket::textMessageReceived, this, &Server::processMessage);
    connect(pSocket, &QWebSocket::disconnected, this, &Server::socketDisconnected);

    //enregistrement team

    QVariant returnedValue;
    QMetaObject::invokeMethod(_oQml, "webSocketServer_message",
                              Q_RETURN_ARG(QVariant, returnedValue),
                              Q_ARG(QVariant, ":connect"));

    QString sReturn= returnedValue.toString();

    QStringList tReturn=sReturn.split("###");
    QString sUserReturn="";
    QString sAllReturn="";

    sUserReturn=tReturn[0];
    sAllReturn=tReturn[1];

    pSocket->sendTextMessage(sUserReturn);

    m_clients << pSocket;
}

On voit que l'on récupère la prochaine connexion socket avec nextPendingConnexion, que l'on ajoute en fin de méthode à un tableau des joueurs connectés :

 
Sélectionnez
m_clients << pSocket;

Sur chaque connexion, nous souhaitons également connaître la couleur du joueur. Pour cela nous invoquons une méthode côté QML :

 
Sélectionnez
QVariant returnedValue;
QMetaObject::invokeMethod(_oQml, "webSocketServer_message",
                              Q_RETURN_ARG(QVariant, returnedValue),
                              Q_ARG(QVariant, ":connect"));

Faisons un saut sur cette méthode QML du fichier main.qml :

 
Sélectionnez
function webSocketServer_message( message ){
    return main.oGame.webSocketServer_receive(message);
}

Comme vous vous en doutiez, cette méthode fait un « pont » vers la partie JavaScript Game.js dont voici un extrait :

 
Sélectionnez
//server
function webSocketServer_receive(message_){

    var sReturn;
    var sUserReturn='';
    var sAllReturn='';

    var tMessage=message_.split(':');
    if(tMessage[0]===''){

        var userTeam=_tTeam[ _iNextTeam ];

        sUserReturn='setTeam:'+userTeam;

        stack.currentItem.webSocketAppendMessage("New User "+userTeam);

        _iNextTeam++;

    }else if(tMessage[1]==='start'){

        sAllReturn="gotoScene;"+_iNextTeam;


    }else{

        stack.currentItem.webSocketAppendMessage(qsTr("Server received message: %1").arg(message_));

        sAllReturn=message_;

    }

    return sUserReturn+'###'+sAllReturn;

}

La partie qui nous intéresse ici est le premier bloc if :

 
Sélectionnez
var tMessage=message_.split(':');
if(tMessage[0]===''){

        var userTeam=_tTeam[ _iNextTeam ];

        sUserReturn='setTeam:'+userTeam;

        stack.currentItem.webSocketAppendMessage("New User "+userTeam);

        _iNextTeam++;

Les messages suivent tous le même formalisme  : couleur du joueur + « : » + action.

Au premier message d'un nouveau joueur, puisqu'il n'a pas de couleur attribuée la première partie est vide : on lui attribue donc une couleur en piochant dans le tableau _tTeam, puis on incrémente une variable pour assigner une autre couleur au prochain joueur.

Dans cette fonction, on retourne une chaîne de caractères contenant deux variables : une instruction pour l'initiateur du message sUserReturn et l'autre pour tous les joueurs sAllReturn que je sépare par un groupe de caractères « ### ».

On renvoie donc un message au nouveau joueur avec comme instruction setTeam, qui correspond à « votre couleur de joueur est… ».

Ensuite, on appelle la fonction côté QML pour indiquer sur l'écran serveur l'arrivée d'un nouveau joueur.

II-E-3. Côté client

À l'ouverture de l'application, quand on choisit de se connecter à une partie, le formulaire demande de renseigner l'adresse de la partie à laquelle se connecter : l'IP et le port du serveur websocket (affiché sur l'écran du joueur hébergeant la partie).

Voici le code concerné dans clientSide.qml :

 
Sélectionnez
Bouton{
    id:btnConnect
    visible:true
    y:main.oGame.convert(610)
    x:main.oGame.convert(40)
    _width: main.oGame.convert(640)
    _height: main.oGame.convert(60)
    _text: qsTr("Se connecter!")
    _link:function(){

        main.oGame._urlWebsocket=serverUrl.text;

        main.oGame.webSocketClient_send("connect");

        connect.start();
    }
}

Pour la partie cliente, à l'action sur le bouton, on commence par enregistrer l'adresse de la partie, puis on appelle la fonction JavaScript pour envoyer des messages : main.oGame.webSocketClient_send().

Avant d'aller voir la fonction JavaScript, précisons que le connect.start() indique de démarrer un Timer pour envoyer un message de test au serveur pour valider la connexion.

Voici la fonction JavaScript contenue dans le fichier Game.js :

 
Sélectionnez
function webSocketClient_send(message_){

    if(false===_bConnected){
        applicationData.connectClient(_urlWebsocket+":"+_urlWebsocketPort);

    }else{
        var iTeamToSend=_tTeamInverse[_sTeam];
        if(_gameStarted){
            if(modelPerso.get(iTeamToSend).visible===true ){
                applicationData.sendMessage(_sTeam+":"+message_);
            }else{
                console.log('gameover can not play');
            }
        }else{
            applicationData.sendMessage(_sTeam+":"+message_);
        }
    }
}

Cette fonction fait deux choses : se connecter (si ce n'est pas déjà fait) et envoyer un message au serveur.

Pour le premier cas, on appelle de nouveau notre fameuse classe C++.

Regardons celle-ci, toujours la classe applicationData contenue dans le fichier, main.cpp :

 
Sélectionnez
Q_INVOKABLE void connectClient(QString url_){
    oClient =new Client(url_,true,oQml);
}

Ici encore, comme pour la partie Serveur, on a sorti le code dans une classe à part.

D'ailleurs, comme pour la partie Serveur, la classe cliente étend la classe QWebSocket. (fichier client.h) :

 
Sélectionnez
#include <QtCore/QObject>
#include <QtWebSockets/QWebSocket>

class Client : public QWebSocket
{
    Q_OBJECT

Son constructeur, en plus d'instancier un objet client websocket, permet de connecter les signaux à des méthodes de la classe, dans le fichier Client.cpp :

 
Sélectionnez
Client::Client( QString url,bool bDebug, QObject * oQml_) : QWebSocket("MyApplication"), isConnecteda(false),_sUrl(url){
    _oQml=oQml_;

    connect(this, &QWebSocket::connected, this, &Client::SL_Connected);
    connect(this, &QWebSocket::disconnected, this, &Client::SL_Disconnected);

    connect(this,&QWebSocket::textMessageReceived,this,&Client::onTextMessageReceived);



    connect(this,static_cast<void(QWebSocket::*)(QAbstractSocket::SocketError)> ( &QWebSocket::error),this,&Client::SL_error);
    open(QUrl(url));
}

Profitons-en pour regarder en détail la méthode onTextMessageReceived :

 
Sélectionnez
void Client::onTextMessageReceived(QString message){

        qDebug() << "!!!Message received:" << message;
        QVariant returnedValue;
        QMetaObject::invokeMethod(_oQml, "webSocketClient_message",
                 Q_RETURN_ARG(QVariant, returnedValue),
                Q_ARG(QVariant, message));

     
}

Comme vous vous en doutiez, elle appelle une fonction QML qui servira de pont pour appeler une fonction JavaScript.

Dans le fichier main.qml :

 
Sélectionnez
function webSocketClient_message(message){
    main.oGame.webSocketClient_receive(message);
}

Et voici la fonction JavaScript webSocketClient_receive dans Game.js qui permet de recevoir les messages du jeu et d'interagir avec la partie :

 
Sélectionnez
//---websocket
//client
function webSocketClient_receive(message_){

   if(message_.substr(0,8)==='setTeam:'){
       _bConnected=true;
       _sTeam=message_.substr(8);

       stack.currentItem.webSocketAppendMessage( "Message: vous êtes l'utilisateur "+_sTeam );
       return;

   }else if(message_.substr(0,9)==='gotoScene'){

       var tStart=message_.split(';');
       _iNbUser=tStart[1];

       gotoScene();
       return;

   }else{
       var tMessage=message_.split(':');
       var iTeamSocket=_tTeamInverse[tMessage[0] ];
       var oPersoSocket=modelPerso.get(_tTeamInverse[tMessage[0] ] );

       if(tMessage[1].substr(0,7)==='restart'){
           var tStart=tMessage[1].split(';');
           _iNbUser=tStart[1];

           gotoScene();
           return;
       }else if(tMessage[1]==='gotoUp' && iCanWalkDirection(oPersoSocket,'up') ){
           oPersoSocket.y-=1;
       }else if(tMessage[1]==='gotoDown' && iCanWalkDirection(oPersoSocket,'down') ){
           oPersoSocket.y+=1;
       }else if(tMessage[1]==='gotoLeft' && iCanWalkDirection(oPersoSocket,'left') ){
           oPersoSocket.x-=1;
       }else if(tMessage[1]==='gotoRight' && iCanWalkDirection(oPersoSocket,'right') ){
           oPersoSocket.x+=1;
       }else if(tMessage[1]==='putBomb'){
           putBomb(oPersoSocket.x,oPersoSocket.y,tMessage[0]);

       }else if(tMessage[1].substr(0,16)==='exploseBombIndex'){
           exploseBombIndex(tMessage[1].substr(17) );
       }else if(tMessage[1].substr(0,10)==='removeBomb'){
           var tArg=tMessage[1].substr(11).split('_');

           removeBomb(tArg[0], tArg[1] );
       }



   }
}

On y voit d'ailleurs la réception du setTeam qui permet d'enregistrer sur l'application quelle couleur nous est attribuée.

Les autres blocs permettent de déplacer des personnages, placer des bombes, changer d'écran…

II-E-4. Mode de communication du jeu

Une fois ces deux parties exposées, il faut comprendre que, contrairement à une application de jeu traditionnelle, à l'action d'un joueur, plutôt que d'agir sur la partie directement, nous envoyons un message au serveur pour qu'il traite et diffuse les interactions du jeu si elles sont acceptées.

Note : les joueurs Serveur et Client utilisent le même processus qui consiste à envoyer des messages à l'hébergeur de la partie.

Par exemple, pour déplacer le joueur vers le haut, on demande au serveur de diffuser la demande de déplacement vers le haut :

 
Sélectionnez
//---gamepad
function clickUp(){
    webSocketClient_send('gotoUp');
}

Dans la fonction webSocketServer_receive du même fichier JavaScript :

 
Sélectionnez
function webSocketServer_receive(message_){

  (...)

  }else{

        stack.currentItem.webSocketAppendMessage(qsTr("Server received message: %1").arg(message_));

        sAllReturn=message_;

    }

    return sUserReturn+'###'+sAllReturn;

}

Comme vous vous en souvenez sûrement, la variable sAllReturn contient le message à envoyer à l'ensemble des clients connectés à la partie (dont le joueur hébergeur).

Ainsi, à chaque fois que nous envoyons un message, le serveur le diffuse et chacun des joueurs reçoit l'instruction et vérifie sa faisabilité, par exemple pour le déplacement vers le haut :

 
Sélectionnez
//---websocket
//client
function webSocketClient_receive(message_){

   (...)

   }else{
       var tMessage=message_.split(':');
       var iTeamSocket=_tTeamInverse[tMessage[0] ];
       
        var oPersoSocket=modelPerso.get(_tTeamInverse[tMessage[0] ] );

       if(tMessage[1].substr(0,7)==='restart'){
           
            (...)

       }else if(tMessage[1]==='gotoUp' && iCanWalkDirection(oPersoSocket,'up') ){
           oPersoSocket.y-=1;

Dans un premier temps, on récupère la couleur du joueur à l'origine de l'instruction, puis, si le joueur en question peut s'y déplacer, on déplace le personnage vers le haut.

J'ai fait le choix que chaque joueur envoie son message au serveur qui se contente de le diffuser, et donc chaque joueur vérifie la faisabilité de l'action, on peut bien sûr facilement inverser ce processus pour éviter le doublon de calcul de faisabilité ainsi que la triche.

II-F. Trucs et astuces, feed-back

En faisant un peu de veille pour cet article, j'ai appris que l'on pouvait régler un petit souci lié à la création d'applications Android avec Qt : l'artefact du lancement.

En effet, au lancement de l'application, on peut avoir un fond vert dû au temps nécessaire pour charger les éléments graphiques : en effet, il charge du plus simple (couleur) au plus complexe (images).

Pour régler ce souci, il faut que votre QML de départ ait un fond de couleur similaire à votre écran de démarrage.

Autre astuce, cette fois pour les performances, ne pas hésiter à mettre du log sur certaines méthodes/parties gourmandes pour vérifier si celles-ci ne sont pas appelées en doublon ou trop de fois sur des périodes courtes. Un profileur QML aurait l'avantage de fournir une visualisation de ces appels, mais est plus difficile d'accès. Par exemple Qt Creator en possède un.

III. Conclusion

Dans ce nouvel article, vous avez pu apprécier la facilité à utiliser de concert C++, QML et JavaScript.

Cette bibliothèque vous offre vraiment un environnement et une méthode de développement flexibles : vous pouvez très bien, par exemple, commencer à développer rapidement votre jeu en JavaScript/QML, puis passer un peu de temps, une fois le gameplay installé, à réécrire cette partie gourmande en C++ pour profiter de ses performances.

Vous pouvez également combiner des composants QML « standards » à l'écriture de certaines parties plus robustes/flexibles en C++, comme ici avec websocket.

Bref, j'espère vous avoir convaincu de vous lancer dans l'aventure du développement Android avec cette bibliothèque.

IV. Remerciements

Je souhaiterais remercier dourouc05 et LittleWhite pour leur relecture technique et surtout leurs conseils et critiques qui m'aident beaucoup à chaque fois autant dans l'écriture de ces articles que dans mon apprentissage de cette bibliothèque, que je redécouvre à chaque projet un peu plus. Je remercierai également ClaudeLELOUP pour sa prompte validation orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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 ni 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.