Création d’un plugin GPS pour Unity (par Matthieu Deru)

Ce tutoriel vous explique comment facilement compiler et créer votre propre plugin Unity (en utilisant Eclipse) pour extraire les données du module GPS sous Android.

Pourquoi utiliser un plugin Unity pour obtenir les données GPS ? En fait en voulant uniquement utiliser les bibliothèques et les fonctions Input.location de Unity pour obtenir la longitude et la latitude, j’ai remarqué avec une certaine déception que ces données n’étaient pas assez précises et parfois complètement biaisées (http://forum.unity3d.com/threads/132587-GPS-Input.Location-accuracy-question), ce qui rendait impossible le calcul direct de la vitesse de mon appareil Android. Cela m’a donc poussé à me pencher sur la programmation et l’intégration d’un plugin GPS pour Android.

La documentation sur internet sur le développement de Plugin sous Unity est malheureusement très sommaire, je tiens cependant à mentionner que les sites suivants ont apportés quelques bribes d’informations pour concevoir ce tutoriel et mieux comprendre la démarche :

http://stackoverflow.com/questions/938719/android-using-locationmanager-does-not-give-a-geo-fix

http://forum.unity3d.com/threads/126241-How-to-use-a-self-plugin-in-unity-project

http://forum.unity3d.com/threads/100751-Android-Plugin-JNI-Question

http://randomactsofdev.wordpress.com/2011/08/19/accessing-the-android-compass-through-unity-3d/

Dans ce tutoriel vous découvrirez également comment à l’aide d’Eclipse et d’un simple script Ant vous pouvez simplifier toutes les étapes du  développement de votre  Plugin.

Prérequis :

Unity 3.5.1 ( la version 3.5 est aussi ok)

Le kit de développement Android

Eclipse (Helios) avec le support ADT.

 

Commençons à développer notre plugin GPS !

Création du fichier Java pour l’accès aux fonctions GPS

Lancez Unity et créez un nouveau projet. Placez une nouvelle caméra dans la scène. Sous l’onglet Project créez un nouveau dossier nommé « Plugins » et ensuite sous ce dossier, le dossier « Android »

http://www.mat-d.com/site/wp-content/uploads/unity_plugin_directory.png

[]

Créez ensuite le fichier Java nommé GPSTest.java et placez-y les lignes de code suivantes:

Notez que vous le code suivant n’arrêtera pas l’acquisition des données GPS si l’application est en pause ou en arrière-plan. Vous pouvez cependant le faire en modifiant les blocs onResume() et onStop().

package com.test.app;

import com.unity3d.player.UnityPlayerActivity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Config;
import android.util.Log;

public class GPSTest extends UnityPlayerActivity {

    private static final String TAG = "GPS_Unity";

    /** Stocke la position actuelle de l’appareil Android */
    public static Location currentLocation;

    public static LocationManager myLocationManager;

    /** Ecouteurs pour obtenir la position actuelle à partir du module GPS et à partir du réseau */
    static LocationListener networkLocationListener;
    static LocationListener gpsLocationListener;

    /** Démarrage du module GPS */
    public static void startLocationListeners() {
        /**
         * Gps location listener.
         */
        gpsLocationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                currentLocation = location;
                Log.i(TAG, "Getting Location over GPS " + currentLocation.toString());
            }

            public void onProviderDisabled(String provider) {
            }

            public void onProviderEnabled(String provider) {
            }

            public void onStatusChanged(String provider, int status,
                    Bundle extras) {
            }
        };

        /**
         * Ce bloc écoute les mises à jour de la position de à partir du réseau
         */
        networkLocationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                currentLocation = location;

                Log.i(TAG,
                        "Getting Location over GPS" + currentLocation.toString());
            }

            public void onProviderDisabled(String provider) {
            }

            public void onProviderEnabled(String provider) {
            }

            public void onStatusChanged(String provider, int status,
                    Bundle extras) {
            }
        };

        myLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,0, 0,
                networkLocationListener);
        myLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
                gpsLocationListener);
    }

    @Override
    protected void onCreate(Bundle myBundle) {

        super.onCreate(myBundle);
        myLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        // Démarre les écouteurs
        startLocationListeners();
    }

    @Override
    protected void onResume() {
        if (Config.DEBUG)
            Log.d(TAG, "onResume");

        super.onResume();
        startLocationListeners();
    }

    @Override
    protected void onPause()
    {
        myLocationManager.removeUpdates(networkLocationListener);
        myLocationManager.removeUpdates(gpsLocationListener);

        super.onPause();

    }

    @Override
    protected void onStop() {
        if (Config.DEBUG)
            Log.d(TAG, "onStop");
        myLocationManager.removeUpdates(networkLocationListener);
        myLocationManager.removeUpdates(gpsLocationListener);
        super.onStop();
    }

    /** Retourne la vitesse actuelle en km/h */
    public static String getSpeed()
    {
        if(currentLocation!=null)
            return "" + currentLocation.getSpeed()*3.6;
        else
            return "Unknown";

    }
}

Important: vérifiez bien le nom du paquet Java (com.test.app). Ce dernier doit être le même que celui spécifié dans le champ (Bundle Identifer)  sous l’onglet Player Settings dans Unity.

http://www.mat-d.com/site/wp-content/uploads/unity_bundle_identifier-e1335612468382.png

Si ce nom de paquet ne correspond pas, vous obtiendrez des problèmes d’appel car la définition de votre classe ne sera pas trouvée par la machine virtuelle et vous obtiendrez directement un crash en démarrant votre application. Vous pouvez vérifier tout cela grâce à l’outil DDMS qui se trouvent sous /android-sdk/tools/ddms.bat

Explication du code

La première étape consiste à faire hériter votre classe de UnityPlayerActivity. En compilant à travers l’option Build & Run de Unity, un package (APK) est créé. Celui-ci contient une instance du player Unity qui va ensuite lancer sa propre Activity. Cette Activity va ensuite charger votre scène 3D et l’afficher sur l’écran. Ensuite comme nous voulons lancer nos propres fonctions notamment l’écoute des données GPS nous allons dire au compileur qu’il doit intégrer notre nouvelle classe. En temps normal lorsqu’on développe une application Android le layout (R.layout.main) (la description de l’interface) est chargée par la fonction  setContentView(int) dans le bloc onCreate(). Nous n’utiliserons pas cette fonction car nous voulons uniquement charger le contenu de notre scène 3D Unity.

Lorsque onCreate() est appelé nous démarrons les écouteurs des modules GPS et de réseau (il est cependant très peu probable que le positionnement à travers le réseau vous livre une vitesse). Dès qu’une nouvelle position est détectée la fonction onLocationChanged est appelée. Cette fonction va stocker dans la variable currentLocation la position actuelle (notamment la longitude, la latitude et la vitesse) provenant du module GPS. Cette variable currentLocation va être ensuite renvoyée à Unity.

Si vous cherchez une version plus robuste pour obtenir la position actuelle je vous conseille de jeter un petit coup d’œil au code source suivant :

http://stackoverflow.com/questions/3145089/what-is-the-simplest-and-most-robust-way-to-get-the-users-current-location-in-a/3145655#3145655

Configuration du fichier manifest.xml

A l’instar de n’importe quelle application Android, nous devons créer et configurer un fichier nommé AndroidManifest.xml. Ce fichier manifest va déterminer au moment de la compilation quelles classes et quelles fonctions de l’appareil Android ont le droit d’être appelées. Dans notre cas nous devons spécifier le même nom de l’Activity que le nom de notre fichier jar compilé. Ne vous inquiétez pas pour le .jar, nous allons voir dans le prochain paragraphe comment le générer et le compiler.

Créez sous /Plugins/Android un fichier nommé AndroidManifest.xml et placez-ici le contenu suivant :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.app"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="8" />
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-feature android:name="android.hardware.location.gps" android:required="true" />
    <application android:label="@string/app_name">
        <activity android:name=".GPSTest"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Notez bien la ligne suivante

 <activity android:name=".GPSTest">

La valeur de l’attribut android:name doit correspondre au “Bundle identifier” de votre application et doit correspondre au nom de paquet spécifié dans GPSTest.java.

package="com.test.app"

Permissions

Afin de pouvoir accéder au module GPS et déterminer notre position à travers le réseau, il faut explicitement autoriser l’accès en ajoutant les uses-permission pour ACCESS_FINE_LOCATION et  ACCESS_COARSE_LOCATION.

  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-feature android:name="android.hardware.location.gps" android:required="true" />

Si votre plugin doit utiliser des APIs spécifiques à Honeycomb ou Ice-Cream Sandwich, vous devrez encore modifier la ligne suivante avec le numéro de version SDK souhaitée:

android:minSdkVersion="8"

À ce stade nous avons donc spécifié quelle Activity devait être lancée. Lorsque nous cliquerons sur Build and Run, Unity va vérifier le contenu de notre manifest (Unity ne prendra pas son manifest par défaut) et ensuite va regarder quelle Activity (donc .jar) doit être lancée et va ensuite le compiler dans le paquet APK (*.apk-file) avant de l’installer sur l’appareil Android.

Compilation du .jar avec ANT

Nous avons le fichier AndroidManifest.xml et  GPSTest.java placés sous /Plugins/Android

http://www.mat-d.com/site/wp-content/uploads/Capture_unity_structure.png

Au lieu de devoir taper de longues ligne de commande avec cmd et javac (ce qui peut être délicat et pénible surtout si l’on a des options et des classpathes à spécifier), nous allons simplifier le tout à l’aide d’un script Ant. Pour ce script je me suis inspiré du script présent sous (http://unity3d.com/support/documentation/Images/manual/AndroidJavaPlugin.zip) en l’adaptant un peu et le rendant plus compact pour les besoins de ce tutoriel

Ant permet de créer assez rapidement des scripts qui créeront des dossiers, appelleront des exécutables ou dans notre cas, compilera et générera notre classe. En plus de cela un projet Ant peut être directement importé dans Eclipse.

N.B: si vous utilisez des classes ou des bibliothèques externes, vous serez obligé d’adapter quelque peu le script Ant suivant. Vous pouvez vous aider de la documentation officielle (http://ant.apache.org/manual/)

Créez un fichier nommé build.xml sous /Plugins/Android/ avec le contenu suivant :

<?xml version="1.0" encoding="UTF-8"?>
<project name="CompileGPSAndroidJava">

    <!—Changez les chemins pour qu’ils correspondent à votre configuration -->
    <property name="sdk.dir" value="C:\android-sdk-windows"/>
    <property name="target" value="android-8"/>
    <property name="unity.androidplayer.jarfile" value="C:\Program Files (x86)\Unity\Editor\Data\PlaybackEngines\androiddevelopmentplayer\bin\classes.jar"/>

    <!—Dossier où se trouvent les sources -->
    <property name="source.dir" value="." />

    <!—Dossier pour les fichiers .class ->
    <property name="output.dir" value="./classes"/>

    <!—Nom du fichier jar qui doit être créé. Notez que le nom doit correspondre au nom de la classe créée et au nom spécifié dans le fichier
AndroidManifest.xml-->
    <property name="output.jarfile" value="GPSTest.jar"/>

      <!—Création des répertoires si inexistants-->
    <target name="-dirs"  depends="message">
        <echo>Creating output directory: ${output.dir} </echo>
        <mkdir dir="${output.dir}" />
    </target>

   <!-- Compile les fichiers .java files vers un fichier .class-->
    <target name="compile" depends="-dirs"
                description="Compiles project's .java files into .class files">
        <javac encoding="ascii" target="1.6" debug="true" destdir="${output.dir}" verbose="${verbose}" includeantruntime="false">
            <src path="${source.dir}" />
            <classpath>
                <pathelement location="${sdk.dir}\platforms\${target}\android.jar"/>
                <pathelement location="${unity.androidplayer.jarfile}"/>
            </classpath>
        </javac>
    </target>

    <target name="build-jar" depends="compile">
        <zip zipfile="${output.jarfile}"
            basedir="${output.dir}" />
    </target>

    <target name="clean-post-jar">
         <echo>Removing post-build-jar-clean</echo>
         <delete dir="${output.dir}"/>
    </target>

    <target name="clean" description="Removes output files created by other targets.">
        <delete dir="${output.dir}" verbose="${verbose}" />
    </target>

    <target name="message">
     <echo>Android Ant Build for Unity Android Plugin</echo>
        <echo>   message:      Displays this message.</echo>
  <echo>   </echo>
        <echo>   build-jar: Compiles project's .class files into .jar file.</echo>
    </target>

</project>

N.B: vous devez changer les chemins des variables source.dir, output.dir et bien sûr le nom du *.jar généré output.jarfile.

Version 1: À l’aide de la commande cmd. Le SDK d’Android inclus une distribution de Ant. Donc vous pouvez directement lancer le script Ant à l’aide de la ligne de commande suivante: Ouvrez une ligne de commande (cmd) ou votre Terminal et entrez la commande suivante:

ant build-jar clean-post-jar

Après quelques secondes vous obtiendrez le message suivant vous indiquant que tout a été correctement exécuté !

http://www.mat-d.com/site/wp-content/uploads/ant_build_output-console-1024x370.png

À ce stade vous pouvez directement aller au paragraphe Implémentation du code  C# pour l’appel des fonctions Java.

Version 2: Avec Eclipse. L’intégration dans Eclipse est l’une des meilleures solutions pour implémenter des plugins, car vous aurez rapidement accès à l’auto-complétion, mais également directement accès aux fonctions Java d’Android. Vous pourrez donc mieux implémenter et tester votre code. http://www.mat-d.com/site/wp-includes/js/tinymce/plugins/wordpress/img/trans.gif

Démarrez Eclipse

Sous File ->New sélectionnez New Java Project from Existing Ant Buildfile et ensuite pressez sur Next

http://www.mat-d.com/site/wp-content/uploads/eclipse_select_ant_wizard.png

Dans la boîte de choix, sélectionnez Java Project from an Ant Buildfile et cliquez sur Browse pour sélectionner votre fichier nommé build.xml

http://www.mat-d.com/site/wp-content/uploads/eclipse_new_from_ant_buildfile.png

Une fois le fichier build.xml sélectionné appuyez sur Finish

Nous obtenons la structure de fichier suivante, mais hélas il y a un petit problème : http://www.mat-d.com/site/wp-content/uploads/eclipse_generated_workspace.png

Nous avons deux fichiers build.xml. En fait le Wizard a automatiquement généré un fichier build.xml qui pointe sur le workspace et non sur notre propre fichier build.xml qui lui est placé dans notre projet Unity. Supprimez ce fichier build.xml et gardez uniquement le vôtre situé dans le dossier /Android (de votre projet Unity)

Si cela n’était pas le cas, changez le nom du paquet par défaut et spécifiez le nouveau paquet pour le fichier GPSTest.java.

http://www.mat-d.com/site/wp-content/uploads/eclipse_unity_activity_change_package.png

Vous devriez avoir la structure suivante. Vous pouvez supprimer le répertoire classes

http://www.mat-d.com/site/wp-content/uploads/eclipse_correct_project_structure.png

Notre projet a maintenant une structure correcte. Nous pouvons maintenant directement modifier le fichier GPSTest.java. Vous remarquerez que vous obtenez directement l’aide contextuelle ainsi que l’auto-complétion sous Eclipse. C’est bien pratique n’est-ce pas ?

Maintenant nous allons lancer notre fichier Ant. Allez sur External Tools -> Run As ->Ant Build..

http://www.mat-d.com/site/wp-content/uploads/build_ant.jpg

Une fenêtre va s’ouvrir avec différentes options. Cliquez sur l’onglet “Main”. Le chemin est faux car nous voulons en fait qu’il pointe vers notre dossier Plugins/Android. Changez-le en appuyant sur “Browse File System”:

http://www.mat-d.com/site/wp-content/uploads/eclipse_path_build_ant_target_correct_path.png

Sélectionnez l’onglet “Targets” tab et cliquez sur build-jar et ensuite clean-post-jar.

http://www.mat-d.com/site/wp-content/uploads/eclipse_path_build_ant_target.png

Pressez sur “Apply” et “Run”.

http://www.mat-d.com/site/wp-content/uploads/eclipse_ant_build_successful_output.png

Youpi! Vous venez de compiler votre fichier jar. Notez la présence du fichier GPSTest.jar dans le répertoire.

Vous pouvez quitter Eclipse et lancer à nouveau Unity. Si tout s’est bien déroulé vous devriez obtenir la hiérarchie de projet suivante:

http://www.mat-d.com/site/wp-content/uploads/Capture_unity_structure.png

Implémentation du code  C# pour l’appel des fonctions Java.

Dans Unity nous allons créer un nouveau script C#. Donnez-lui le nom GPSManager.cs et collez le code suivant dans ce fichier.

using UnityEngine;
using System.Collections;

public class GPSManager : MonoBehaviour {

    static string speedMessage;

    AndroidJavaClass gpsActivityJavaClass;

    void Start () {
        AndroidJNI.AttachCurrentThread();
        gpsActivityJavaClass = new AndroidJavaClass("com.test.app.GPSTest");
    }

    void Update() {

        speedMessage = gpsActivityJavaClass.CallStatic<string>("getSpeed");

        float speed = 0;
        if(speedMessage!="Unknown")
        {
            speed = float.Parse(speedMessage);
            GameObject.Find("gps_output").guiText.text = speed + "km/h";

        }
        else
        {
            GameObject.Find("gps_output").guiText.text = "No speed :-(";
        }
    }
}

Le code est assez simple à comprendre: nous indiquons à la VM qu’elle doit prendre la référence d’une de nos classe. Dans notre cas de figure nous allons demander une référence sur la classe Java nommée (« com.test.app.GPSTest« ). Une fois trouvée nous pouvons appeler la fonction statique getSpeed() de GPSTest.java

Comme nous savons que cette fonction est bien statique nous appelons la fonction CallStatic de notre objet de type AndroidJavaClass.

Notons que le type de retour doit toujours correspondre, sous peine d’avoir des erreurs lors de l’appel de la fonction sous C#.

speedMessage = gpsActivityJavaClass.CallStatic<string>("getSpeed")

À ce stade nous avons donc obtenu de l’appel de la fonction statique getSpeed() une valeur de type string que nous stockons dans une variable nommée speedMessage. Dans le cas où cette variable contient une valeur parsable de type float, nous pouvons directement extraire cette valeur à l’aide de la ligne suivante :

speed = float.Parse(speedMessage);

Dans un souci de performance nous pouvons également utiliser la méthode Java UnitySendMessage pour appeler directement à partir du code Java une fonction C# (par exemple setSpeed si implémentée du côté C#). Cela évite de faire un appel au code Java à chaque Update() de C#. Nous pouvons donc écrire cela dans notre code Java (à condition d’avoir la méthode setSpeed(int) côté C#)

UnityPlayer.UnitySendMessage("Main Camera","setSpeed",Integer.toString())

Créez un objet de type GUI Text et nommez le gps_output

http://www.mat-d.com/site/wp-content/uploads/final_unity_project_structure.png

Structure finale du projet Unity GPS avec le plugin

Attachez ce script à Main Camera: le tour est joué !

Etape finale: Build et Run !

Si vous avez bien suivi pas à pas les étapes de ce tutoriel vous obtiendrez après compilation sous Unity un magnifique écran bleu avec un petit texte. Prenez votre appareil Android sortez dehors ou roulez un peu en voiture et vous verrez apparaître la vitesse actuelle sur l’écran de votre appareil !

Par curiosité vous pouvez lancer l’outil DDMS et verrez les logs suivants dès qu’une nouvelle position a été détectée:

http://www.mat-d.com/site/wp-content/uploads/logsSpeed.png

Voici l’ensemble des fichiers à télécharger -)

GPSAndroidPlugin_Example.zip

Ça ne marche pas !!! – Petit guide des erreurs fréquentes.

Voici une petite liste des erreurs les plus fréquentes quand on développe un plugin pour Unity

  1. Le nom des paquets est faux et ne correspond pas au Bundle Identifier spécifié dans Unity
  2. Le nom de l’Activity dans le fichier manifest AndroidManifest.xml ne correspond pas au nom du fichier JAR qui vient d’être compilé.
  3. Appel de CallStatic avec les mauvais paramètres et types de retours
  4. Le nom de la classe spécifié pour AndroidJavaClass n’est pas correct (ClassNotFound Exception)

http://www.mat-d.com/site/wp-content/uploads/wrong.jpg

  1. Les permissions dans Android Manifest.xml sont manquantes
  2. Si vous travaillez sous Eclipse vérifiez bien les chemins des fichiers que vous éditez. Tous les chemins doivent pointer vers /Plugins/Android et non vers le workspace d’Eclipse.
  3. Les fonctions C# appelées à travers UnitySendMessage, sont manquantes ou ne possèdent pas de type de retour.

Et n’oubliez pas, la plupart du temps l’outil DDMS et la lecture des logs vous permettront  d’identifier assez rapidement l’origine du problème ! Bonne programmation !

Matthieu Deru

avatar

krys64

bla bla bla

Vous aimerez aussi...

6 réponses

  1. avatar ZJP dit :

    Merci et bien joué. J’avais une autre solution qui n’était malheureusement pas compatible avec Unity Android (version non Pro).

    JP

  2. avatar ZJP dit :

    PS :
    L’icône de l’application n’est pas prise en compte. Il faut modifier l’AndroidManifest :

  3. avatar ZJP dit :

     » « 

  4. avatar marie dit :

    Pourquoi on utilise eclipse ? prquoi on peux pas utiliser que Unity et Le kit de développement Android?

  5. avatar TropikalG dit :

    Bonsoir,je voudrai savoir si ce tutoriel fonctionne avec les nouvelles version d’Unity?( Unity 5.4 dans mon cas)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.