Tutorial: créez des applications avec HTML5 sur Windows Phone grâce à PhoneGap

Tout d’abord, nous allons naturellement commencer par voir l’intérêt d’utiliser PhoneGap pour HTML5. Ensuite, nous verrons comment créer son tout 1er projet et récupérer les valeurs de l’accéléromètre en JavaScript. Pour finir, nous allons voir un exemple complet où j’ai repris le code de mon jeu HTML5 Platformer pour le faire fonctionner presque tel quel avec l’accéléromètre d’un Windows Phone.

  1. Introduction
  2. PhoneGap : un framework qui comble les manques
  3. Création de son 1er projet PhoneGap sur Windows Phone
  4. Récupération des valeurs de l’accéléromètre
  5. Exemple complet avec le jeu HTML5 Platformer
    1. Forcer le mode paysage
    2. Adapter le code pour gérer plusieurs résolutions
    3. Chargement des niveaux via l’appel au système de fichiers plutôt que XHR
    4. Modification du gameplay pour gérer l’accéléromètre
    5. Le résultat en images avec quelques FPS sur les téléphones
    6. Solution complète à télécharger
  6. Conclusion

Introduction

imageLa mise à jour Mango sur Windows Phone a apporté le support d’HTML5 via l’arrivée du navigateur IE9. Tout comme sur la version bureau, la version mobile d’IE9 met en place l’accélération matérielle via l’utilisation du GPU embarqué sur votre Windows Phone. Ainsi, couplé à JavaScript, IE9 permet d’envisager la réalisation d’expériences utilisateurs intéressantes autre fois uniquement réservées aux applications dites “natives”.

L’avantage d’utiliser HTML5 comme plateforme de développement est qu’elle permet une facilité “relative” dans la réutilisation d’une partie du code sur d’autres plateformes comme Android ou iOS. HTML5 suscite donc énormément d’intérêt de l’écosystème ces derniers temps pour les plateformes mobiles. Parfois malheureusement à tord mais j’y reviendrais surement dans un autre billet.

Cependant, bien que les spécifications HTML5/CSS3/SVG et JavaScript aient nettement évoluées ces derniers mois, il existe des manques majeurs pour la réalisation d’applications mobiles. En effet, un téléphone ou une tablette dispose de capacités spécifiques à leur mobilité : GPS, accéléromètre, caméra, SMS, accès aux contacts, etc.

Pour accéder à ces spécificités matérielles depuis son code JavaScript, le W3C travaille depuis un moment sur ce que nous appelons les “Device APIs” ou DAP. Malheureusement, on peut considérer qu’il n’existe à l’heure actuelle aucune implémentation de ces spécifications sur le marché comme le confirme ce document : Standards for Web Applications on Mobile: November 2011 current state and roadmap . Mozilla a commencé un travail intéressant sur une forme de fork de ces spécifications nommées Web APIs pour soutenir son projet Boot To Gecko. C’est donc une bonne nouvelle dans la mesure où une forme d’implémentation commence à voir le jour et un dialogue avec le W3C semble s’installer. Les choses commencent doucement à bouger mais il faudra probablement plusieurs années avant de voir une spécification du W3C implémentée et répandue sur toutes les plateformes.

Alors comment faire en attendant ? HTML5 peut-il vraiment adresser ce scénario ?

PhoneGap : un framework qui comble les manques

En attendant l’arrivée de réelles spécifications standardisées, nous n’avons donc pas le choix : il faut créer un pont entre le code JavaScript et le code natif de la plateforme visée pour accéder au matériel. L’idée est donc la suivante : on prend le langage natif de chacune des plateformes (Silverlight, Objective-C et Java) et on en créé un framework qui expose des interfaces pour le développeur JavaScript.

C’est exactement ce que fait PhoneGap. Prenons le cas de Windows Phone qui nous intéresse dans cet article. Un projet PhoneGap expose tout simplement une application Silverlight hébergeant le contrôle WebBrowser (et donc IE9) ainsi qu’une DLL écrite en C# s’occupant d’accéder à l’accéléromètre, au GPS, aux contacts, à la caméra, etc. Ainsi, comme on va le voir un peu plus tard, en tant que développeur JavaScript, vous allez donc utiliser indirectement une DLL nommée WP7GapClassLib.dll (le coeur de PhoneGap) à travers le code exposé dans le fichier phonegap-1.3.0.js. La DLL contient le code C# qui fait appel au runtime Silverlight présent sur le téléphone. Ce dernier peut accéder à l’accéléromètre et au reste du téléphone. La librairie JavaScript contenu dans le .js va faire office d’interface entre les 2 mondes. L’avantage de faire appel à cette librairie est que le code JavaScript que vous allez écrire pour accéder à la caméra du Windows Phone par exemple marchera tel quel sur PhoneGap pour Android ou iOS. PhoneGap offre donc une certaine forme de portabilité.

Sachez d’ailleurs qu’à ce sujet, le support de PhoneGap pour Windows Phone est désormais aussi complet que sur Android ou iOS depuis la récente version 1.3 :

Pour finir, PhoneGap vous fournit également un autre service intéressant. Il vous permet d’inclure vos fichiers JS, CSS, HTML, PNG & co au sein de son projet pour packager l’ensemble comme une véritable application native. En résumé, vous pouvez donc tout à fait utiliser PhoneGap pour packager une application HTML5 que vous déploierez sur le Store. C’est le cas par exemple de l’application SujiQ qui a été conçue ainsi.

Création de son 1er projet PhoneGap sur Windows Phone

Pré-requis

Voici les étapes à suivre avant toute chose :

  1. Téléchargez le SDK Windows Phone si ce n’est déjà fait ici : Télécharger le SDK Windows Phone
  2. Téléchargez la dernière version (1.3 aujourd’hui) de PhoneGap sur leur site : https://phonegap.com/
  3. Décompressez l’archive téléchargée depuis le site de PhoneGap
  4. Copiez les fichiers PhoneGapStarter.zip et PhoneGapCustom.zip dans \Documents\Visual Studio 2010\Templates\ProjectTemplates

Nouveau projet

Une fois toutes ces étapes suivies correctement, nous allons pouvoir créer notre 1er projet PhoneGap. Pour cela, démarrez Visual Studio 2010, rendez-vous dans la partie “Visual C#”, filtrez sur le mot clé “Gap” et vous devriez voir un nouveau type de projet nommé PhoneGapStarter :

image

Nommez le projet “MonPremierProjetPhoneGap”. Une fois créé, vous allez retrouver les fichiers dont je vous parlais avant dans l’explorateur de solution :

image

Vous n’avez donc plus qu’à insérer votre application HTML5 dans le répertoire “www”.

Au passage, j’ai plusieurs conseils à vous fournir au sujet de ce projet par défaut :

- ne touchez jamais au fichier phonegap-1.3.0.js si vous souhaitez garder un code portable sur les autres versions de Phone Gap
- tous les fichiers que vous rajouterez dans le répertoire “www” devront être marqué en tant que “Content” dans leurs propriétés 
- à la place du binaire WP7GapClassLib.dll,vous pouvez référencer le projet C# WP7GapClassLib.csproj contenu dans le répertoire “Windows Phone\framework” de l’archive PhoneGap téléchargée. Cela vous permettra de débugger ou parcourir le code de la librairie PhoneGap si besoin.

Allez, commençons par effectuer une action normalement impossible par défaut sous IE9 Mango : récupérer les valeurs de l’accéléromètre en JavaScript.

Récupération des valeurs de l’accéléromètre

Nous allons voir comment, de manière très simple, récupérer les valeurs renvoyées par l’accéléromètre (de l’émulateur ou du vrai périphérique).

Rendez-vous dans “index.html” et changer le contenu du body par défaut par celui-ci :

 <body>
    <h1>Démo accéléromètre</h1>
    <div id="valueX"></div>
    <div id="valueY"></div>
    <div id="valueZ"></div>
</body>

On va simplement utiliser 3 <div> qui afficheront les valeurs courantes X, Y et Z de l’accéléromètre.

Ensuite, changer le dernier bloc de <script> par celui-ci :

 <script type="text/javascript">
    document.addEventListener("deviceready", onDeviceReady, false);

    // variable to output the current x, y & z values of the accelerometer
    var valueX;
    var valueY;
    var valueZ;

    // when PhoneGap tells us everything is ready, start watching the accelerometer
    function onDeviceReady() {
        valueX = document.getElementById("valueX");
        valueY = document.getElementById("valueY");
        valueZ = document.getElementById("valueZ");
        startWatch();
    }

    // start monitoring the state of the accelerometer
    function startWatch() {
        var options = { frequency: 500 };
        navigator.accelerometer.watchAcceleration(onSuccess, onError, options);
    }

    // if the z-axis has moved outside of our sensitivity threshold, move the aarvark's head in the appropriate direction
    function onSuccess(acceleration) {
        valueX.innerHTML = "X: " + acceleration.x;
        valueY.innerHTML = "Y: " + acceleration.y;
        valueZ.innerHTML = "Z: " + acceleration.z;
    }

    function onError() {
        alert('onError!');
    }
</script>

Le code est assez simple à lire je pense. Avant toute chose, vous devez attendre que l’évènement “deviceready” soit levé par PhoneGap pour être de vous retrouver dans un état stable. On commence donc par s’abonner à cet évènement et nous serons rappelé sur la fonction OnDeviceReady() .Dans celle-ci, on récupère les 3 instances de <div> qui nous intéresse et on demande ensuite à être notifié des changements de l’accéléromètre toutes les 500ms dans la fonction startWatch() . Si de nouvelles valeurs sont générées, la fonction onSuccess() sera appelée avec les valeurs x, y et z fournies dans l’object acceleration. Vous trouverez toute la documentation sur le site de PhoneGap : PhoneGap Documentation - API Reference - Accelerometer

C’est tout ce qu’il faut faire du côté code JavaScript. Mais pour que cela fonctionne, il faut indiquer dans les propriétés du projet que l’on a besoin d’accéder aux capteurs (sensors) de l’appareil. La liste des capacités nécessaires au bon fonctionnement de notre application se trouve dans le fichier WMAppManifest.xml présent dans le répertoire “Properties”. Par défaut, depuis la version 1.3 de PhoneGap, la liste est limitée au strict nécessaire :

 <Capabilities>
  <Capability Name="ID_CAP_IDENTITY_DEVICE" />
  <Capability Name="ID_CAP_IDENTITY_USER" />
  <Capability Name="ID_CAP_LOCATION" />
  <Capability Name="ID_CAP_NETWORKING" />
  <Capability Name="ID_CAP_WEBBROWSERCOMPONENT" />
</Capabilities>

A vous ensuite d’ajouter ce dont vous avez besoin dans votre application PhoneGap. Dans notre cas, ajoutez cette ligne :

 <Capability Name="ID_CAP_SENSORS" />

Pour pouvoir autoriser l’accès à l’accéléromètre. Vous retrouverez la liste de toutes ces autorisations ici : Application Manifest File for Windows Phone

Bon, on est logiquement prêt à tester tout cela dans l’émulateur dans un 1er temps. Appuyez sur la touche magique “F5” et observons le résultat :

image

En faisant tourner le téléphone virtuel dans l’émulateur, vous devriez bien voir les valeurs de l’accéléromètre bouger. Félicitations !

Vous pouvez récupérer le code complet de cette solution ici : https://david.blob.core.windows.net/html5/MonPremierProjetPhoneGap.zip

Petit problème sur les téléphones configurés en français

Si vous testez le même code sur votre téléphone et que vous avez une locale en français, vous aurez l’impression que l’application ne fonctionne pas. En débuggant un peu le code, je me suis aperçu que l’exception suivante était levée dans phonegap-1.3.0.js :

"Error in success callback: Accelerometer = Syntax error"

Et en dumpant les valeurs, je me suis aperçu que l’erreur venait d’une tentative de dé-sérialisation JSON de la chaine suivante :

"{\"x\":0,00472,\"y\":-0,19879,\"z\":-0,98115}" au lieu d’avoir celle-ci en EN-US : "{\"x\":0.00472,\"y\":-0.19879,\"z\":-0.98115}"

Et oui, notre fameuse virgule de séparation a toujours l’air d’embêter nos chers amis américains…

2 solutions pour résoudre ce problème :

1 – Passer votre téléphone en EN-US (oui, je sais, c’est tout pourri comme solution)

2 – Corriger la source du problème dans le code C#. Pour cela, remplacez le code suivant présent dans Accelometer.cs de la librairie WP7GalClassLib :

 /// <summary>
/// Formats current coordinates into JSON format
/// </summary>
/// <returns>Coordinates in JSON format</returns>
private string GetCurrentAccelerationFormatted()
{
    string resultCoordinates = String.Format("\"x\":{0},\"y\":{1},\"z\":{2}",
                    accelerometer.CurrentValue.Acceleration.X.ToString("0.00000"),
                    accelerometer.CurrentValue.Acceleration.Y.ToString("0.00000"),
                    accelerometer.CurrentValue.Acceleration.Z.ToString("0.00000"));
    resultCoordinates = "{" + resultCoordinates + "}";
    return resultCoordinates;
}

Par :

 private string GetCurrentAccelerationFormatted()
{
    string resultCoordinates = String.Format("\"x\":{0},\"y\":{1},\"z\":{2}",
             accelerometer.CurrentValue.Acceleration.X.ToString("0.00000", CultureInfo.InvariantCulture),
             accelerometer.CurrentValue.Acceleration.Y.ToString("0.00000", CultureInfo.InvariantCulture),
             accelerometer.CurrentValue.Acceleration.Z.ToString("0.00000", CultureInfo.InvariantCulture));
    resultCoordinates = "{" + resultCoordinates + "}";
    return resultCoordinates;
}

Et désormais, l’exemple fonctionnera avec toutes les locales. J’ai ouvert un bug à ce sujet ici: https://issues.apache.org/jira/browse/CB-141 en proposant la solution ci-dessus qui sera intégrée dans la prochaine version (2.0.0).

Au passage, pour débugger un projet PhoneGap, vous pouvez très bien utiliser jsConsole et toutes les astuces décrites dans mon article précédent : Testez et débuggez vos sites HTML5 sous Windows Phone 7 avec l’émulateur gratuit

Exemple complet avec le jeu HTML5 Platformer

Pour conclure, essayons de mixer tout cela avec un jeu complet que j’avais réalisé dans l’article précédent : HTML5 Platformer: portage complet du jeu XNA vers <canvas> grâce à EaselJS

Voici les étapes que j’ai suivies pour faire fonctionner l’ensemble. La toute première étape consiste simplement à copier/coller les fichiers .js, .png, .css & co dans le répertoire “www” et de bien les marquer comme “Content”. Ensuite, voici les grandes étapes à suivre.

Forcer le mode paysage

Tout d’abord, j’ai forcé l’orientation en mode portrait et retiré la barre d’informations qui prend de la place sur le côté (System Tray) en me rendant de le fichier MainPage.xaml et en changeant ces propriétés :

 SupportedOrientations="Landscape" Orientation="Landscape" shell:SystemTray.IsVisible="False"

Adapter le code pour gérer plusieurs résolutions

Le but étant d’être portable sur le plus grand nombre de périphériques possibles, il faut envisager de dessiner à des résolutions très différentes.

Pour cela, j’ai légèrement modifié le code initial du Platformer que vous pouvez retrouver dans l’autre article. Le jeu est ainsi désormais capable de s’adapter à n’importe quelle résolution en appliquant un ratio sur la manière de dessiner nos images et nos sprites. Tout est calculé pour partir de la résolution d’origine du Windows Phone (800x480) pour ensuite redessiner en fonction de ce ratio d’origine quelque soit la résolution. Vous pouvez le tester dans le navigateur ici : HTML5 Platformer ReScale et vous amusez à redimensionner la fenêtre de votre navigateur. Pour finir, si vous êtes sur une résolution 16/9 sur votre moniteur, appuyez sur la touche F11 pour jouer en plein écran ! L’expérience devient vraiment sympa comme le montre cette copie d’écran :

image

On laisse alors le soin au navigateur d’appliquer éventuellement de l’anti-aliasing lors de cette opération de mise à l’échelle. Vous noterez également, selon certains navigateurs, des performances très différentes en fonction de la taille de la fenêtre et donc de la résolution demandée. IE9/IE10 semble, sur ma machine, relativement indifférent au mode plein écran ou aux petites résolutions en affichant un framerate stable de 60 fps.

Chargement des niveaux via l’appel au système de fichiers plutôt que XHR

Dans le code d’origine, les fichiers .TXT associés à chacun des niveaux étaient stockés sur le serveur et récupérés via des appels XmlHttpRequest. C’est un peu dommage d’effectuer un appel XHR local. Donc j’ai remplacé le code initial de la fonction PlatformerGame.prototype.LoadNextLevel par :

 // Loading the next level contained into /level/{x}.txt
PlatformerGame.prototype.LoadNextLevel = function () {
    this.levelIndex = (this.levelIndex + 1) % numberOfLevels;

    // Searching where we are currently hosted
    var nextFileName = "app/www/assets/levels/" + this.levelIndex + ".txt";
    try {
        var instance = this;
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, gotFS, fail);

        function gotFS(fileSystem) {
            fileSystem.root.getFile(nextFileName, null, gotFileEntry, fail);
        }

        function gotFileEntry(fileEntry) {
            fileEntry.file(gotFile, fail);
        }

        function gotFile(file) {
            readAsText(file);
        }

        function readAsText(file) {
            var reader = new FileReader();
            reader.onloadend = function (evt) {
                instance.LoadThisTextLevel(evt.target.result.replace(/[\n\r\t]/g, ''));
            };
            reader.readAsText(file);
        }

        function fail(evt) {
            console.log(evt.target.error.code);
        }
    }
    catch (e) {
        console.log("Error loading level: " + e.message);
        this.LoadThisTextLevel(hardcodedErrorTextLevel);
    }
};

J’ai bêtement repris le code de la documentation de PhoneGap : FileReader . Vous avez donc pleinement accès au système de fichiers du Windows Phone via JavaScript.

Petit tip : pour vous aider à débugger ce qui est présent ou non dans l’Isolated Storage du téléphone, je vous conseille vivement cet outil qui m’a sauvé la vie : IsoStoreSpy écrit par Samuel Blanchard. Une vraie merveille !

Modification du gameplay pour gérer l’accéléromètre

Bon il ne reste plus qu’à mixer les différentes parties de cet article pour arriver au résultat escompté. Pour cela, j’ai ajouté le code suivant dans le constructeur de l’objet Player dans le fichier Player.js :

 var options = { frequency: 500 };
var that = this;

navigator.accelerometer.watchAcceleration(
    function (accelerometer) { that.moveDirectionAccel(accelerometer); },
    function () { console.log("Error with accelerometer"); }, 
    options);

Ensuite, voici la fonction qui sera rappelée suite aux variations de l’accéléromètre :

 Player.prototype.moveDirectionAccel = function(acceleration) {
    var accelValue = -acceleration.y;

    // Move the player with accelerometer
    if (Math.abs(accelValue) > 0.15) {
        // set our movement speed
        this.direction = Math.clamp(accelValue * this.AccelerometerScale, -1, 1);
    }
    else {
        this.direction = 0;
    }
};

Le résultat en images avec quelques FPS sur les téléphones

Tout d’abord, voici le résultat dans l’émulateur :

image

Sur un vrai périphérique, l’expérience varie d’un téléphone à l’autre. Prenons l’exemple du tout 1er niveau. Sur le LG E900, le framerate tourne autour de 22 fps, sur le HTC Radar, nous sommes autour de 31 fps et sur le Nokia Lumia 800 autour de 42 fps.

DSCF4677

Il en résulte un gameplay souvent décevant dans la plupart des cas. En effet, l’utilisation d’un canvas plein écran n’est pour l’instant pas une bonne idée sur ce genre de mobile même si le Nokia semble être le plus à même de gérer ce scénario. J’en avais déjà parlé lors de ma session ParisWeb 2011 sur le développement “cross-device”. Une approche de découpage de la scène de jeux utilisant de plus petits canvas animés ensuite via leurs propriétés CSS serait plus adaptée. C’est ce qui est fait sur la version HTML5 d’Angry Birds par exemple. Certains frameworks de jeux commencent d’ailleurs à envisager de le gérer pour vous. L’idée serait donc de coder 1 fois le jeu avec des APIs de haut niveau et le framework s’occuperait de choisir soit un canvas plein écran, soit un découpage en plusieurs petits canvas. Vous l’aurez compris : l’expérience de jeux HTML5 sur mobile n’en est qu’à ses débuts mais l’avenir s’annonce très intéressant. A suivre donc !

Solution complète à télécharger

Vous trouverez le code source complet du jeu HTML5 Platformer pour PhoneGap ici : HTML5GapPlatformer.zip

Ressources complémentaires

Conclusion

PhoneGap ouvre des perspectives intéressantes pour vous aider à écrire des applications pour le téléphone en reprenant vos compétences existantes en JavaScript, HTML et CSS. Il ne pourra pas forcément couvrir tous les scénarios actuellement adressées par un développement natif (Silverlight ou XNA) mais c’est une voie à envisager si vous souhaitez capitaliser sur les compétences de l’une de vos équipes. Il faudra par contre bien faire attention à vérifier que le type de projet retenu se prête bien aux limitations actuelles d’HTML5.

Il existe aussi la possibilité de mixer les 2 environnements en créant des expériences hybrides : le tronc commun écrit en “HTML5” et le reste par des équipes maitrisant la plateforme native. C’est ainsi que certains plug-ins ont été créés pour pousser un peu loin l’intégration dans l’expérience Metro : Plug-ins PhoneGap pour Windows Phone . Vous y trouverez par exemple la possibilité de mettre à jour les “Tiles” depuis votre code JavaScript.

Pour finir, PhoneGap et l’usage d’HTML5 permet également une portabilité plus simple vers d’autres plateformes… dans une certaines mesure. Cependant, ce sujet précis étant un vaste et passionnant débat remplis de mauvaises idées reçues, j’y reviendrais peut-être dans un autre billet si j’en ai le temps d’ici les TechDays ! Sourire 

David