Corona Native pour Android - utilisation de code Java personnalisé dans un jeu écrit en Corona

Le moteur de jeu Corona vous permet de créer des applications et des jeux multiplateformes. Mais parfois, l'API qu'ils fournissent n'est pas suffisante. Dans de tels cas, il existe Corona Native , qui vous permet d'étendre les fonctionnalités en utilisant du code natif pour chaque plate-forme.


Cet article traitera de l'utilisation de Java dans les projets Corona pour Android


Pour comprendre ce qui se passe dans l'article, vous avez besoin d'une connaissance de base de Java, Lua et du moteur Corona


Pour commencer


Corona et Android Studio doivent être installés sur l'ordinateur


Le dossier d'installation de Corona contient également le modèle de projet: Native \ Project Template \ App. Copiez l'intégralité du dossier et renommez-le au nom de votre projet.


Personnalisation du modèle


Remarque: J'ai utilisé la dernière version publique disponible pour Corona - 2017.3184 . Dans les nouvelles versions, le modèle peut changer et certaines préparations de ce chapitre ne seront plus nécessaires.


Pour Android, nous avons besoin de 2 dossiers à l'intérieur: Corona et Android


Dans le dossier Corona , supprimez Images.xcassets et LaunchScreen.storyboardc - nous n'avons pas besoin de ces dossiers. Dans le fichier main.lua , nous supprimons également tout le code - nous allons commencer à créer le projet à partir de zéro. Si vous souhaitez utiliser un projet existant, remplacez tous les fichiers du dossier Corona par le vôtre


Le dossier Android est un projet terminé pour Android Studio, nous devons l'ouvrir. Le premier message du studio sera "La synchronisation de Gradle a échoué". Besoin de corriger build.gradle:


construire gradle


Pour corriger la situation, ajoutez un lien vers les référentiels dans buildscript. J'ai également changé la version du chemin de classe «com.android.tools.build:gradle» en une version plus récente.


Code Build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.1.3' } repositories { jcenter() google() } } allprojects { repositories { jcenter() google() } } task clean(type: Delete) { delete rootProject.buildDir } 

L'étape suivante consiste à modifier gradle-wrapper.properties . Vous pouvez le modifier manuellement en remplaçant la version de gradle dans distributionUrl . Ou laissez le studio tout faire pour vous.


gradle-wrapper.properties


De plus, vous devez corriger build.gradle pour le module d'application: dans cleanAssets, vous devez ajouter la ligne de suppression "$ projectDir / build / intermediates / jniLibs" , sans laquelle vous devrez faire un projet propre avant chaque démarrage ( extrait d'ici )


Maintenant que la synchronisation est réussie, il n'y a que quelques avertissements liés à la buildToolsVersion obsolète et à l'ancienne syntaxe dans la configuration. Les corriger n'est pas difficile.


Maintenant, dans le studio, nous voyons 2 modules: application et plugin. Renommez l'application (com.mycompany.app) et le plugin (plugin.library) avant de continuer.


Plus loin dans le code, le plugin sera appelé plugin.habrExamplePlugin


Le plugin contient la classe LuaLoader par défaut - il sera responsable de la gestion des appels à partir du code lua. Il y a déjà du code, mais clarifions-le.


Code LuaLoader
 package plugin.habrExamplePlugin; import com.naef.jnlua.JavaFunction; import com.naef.jnlua.LuaState; @SuppressWarnings({"WeakerAccess", "unused"}) public class LuaLoader implements JavaFunction { @Override public int invoke(LuaState luaState) { return 0; } } 

Utiliser le code du plugin à partir du code lua


Corona Native utilise jnlua pour la liaison entre le code java et lua. LuaLoader implémente l'interface jnlua.JavaFunction, donc sa méthode invoke est disponible à partir du code lua. Pour vous assurer que tout est en ordre, ajoutez le code de journalisation à LuaLoader.invoke et créez le plug-in requis dans main.lua


  @Override public int invoke(LuaState luaState) { Log.d("Corona native", "Lua Loader invoke called"); return 0; } 

 local habrPlugin = require("plugin.habrExamplePlugin") print("test:", habrPlugin) 

Après avoir lancé l'application, parmi les journaux, nous verrons les 2 lignes suivantes:


D / Corona native: invocation de Lua Loader appelée
I / Corona: test vrai

Donc, notre application a téléchargé le plugin, et exige des retours vrais. Essayons maintenant de renvoyer une table lua avec des fonctions à partir du code Java.


Pour ajouter des fonctions au module, nous utilisons l'interface jnlua.NamedJavaFunction. Un exemple d'une fonction simple sans arguments et sans valeur de retour:


 class HelloHabrFunction implements NamedJavaFunction { @Override public String getName() { return "helloHabr"; } @Override public int invoke(LuaState L) { Log.d("Corona native", "Hello Habr!"); return 0; } } 

Pour enregistrer notre nouvelle fonction dans lua, nous utilisons la méthode LuaState.register:


 public class LuaLoader implements JavaFunction { @Override public int invoke(LuaState luaState) { Log.d("Corona native", "Lua Loader invoke called"); String libName = luaState.toString(1); //      (  require) NamedJavaFunction[] luaFunctions = new NamedJavaFunction[]{ new HelloHabrFunction(), //     }; luaState.register(libName, luaFunctions); //   ,     //  1        lua . //     require      , require     return 1; } 

Ce code nécessite des explications supplémentaires:


LuaState, un paramètre de la méthode invoke, représente essentiellement un wrapper sur la machine virtuelle Lua (veuillez me corriger si je me trompe). Pour ceux qui savent utiliser le code lua de C, LuaState est le même que le pointeur lua_State en C.


Pour ceux qui veulent se plonger dans la jungle du travail avec lua, je recommande de lire le manuel, en commençant par l'interface du programme d'application


Ainsi, lorsque invoke est appelé, nous obtenons LuaState. Il a une pile qui contient des paramètres passés à notre fonction à partir du code lua. Dans ce cas, il s'agit du nom du module, car LuaLoader est exécuté lorsque l'appel requiert ("plugin.habrExamplePlugin").


Le nombre retourné par invoke montre le nombre de variables de la pile qui seront retournées au code lua. Dans le cas de require, ce nombre n'a aucun effet, mais nous utiliserons cette connaissance plus tard en créant une fonction qui retourne plusieurs valeurs


Ajout de champs au module


En plus des fonctions, nous pouvons également ajouter des champs supplémentaires au module, par exemple, la version:


  luaState.register(libName, luaFunctions); //   ,       luaState.pushString("0.1.2"); //     luaState.setField(-2, "version"); //   version   . 

Dans ce cas, nous avons utilisé l'index -2 pour indiquer que le champ doit être défini pour notre module. Un indice négatif signifie que le décompte commence à la fin de la pile. -1 pointera vers la chaîne "0.1.2" (en lua, les index commencent par un).


Afin de ne pas obstruer la pile, après avoir défini le champ, je recommande d'appeler luaState.pop (1) - jette 1 élément de la pile.


Code LuaLoader complet
 @SuppressWarnings({"WeakerAccess", "unused"}) public class LuaLoader implements JavaFunction { @Override public int invoke(LuaState luaState) { Log.d("Corona native", "Lua Loader invoke called"); String libName = luaState.toString(1); //      (  require) NamedJavaFunction[] luaFunctions = new NamedJavaFunction[]{ new HelloHabrFunction(), //     }; luaState.register(libName, luaFunctions); //   ,     luaState.register(libName, luaFunctions); //   ,       luaState.pushString("0.1.2"); //     luaState.setField(-2, "version"); //   version   . //  1        lua . //     require      , require     return 0; } } 

Exemples de fonctions


Un exemple de fonction qui prend plusieurs chaînes et les concatène via le générateur de chaînes

Réalisation:


 class StringJoinFunction implements NamedJavaFunction{ @Override public String getName() { return "stringJoin"; } @Override public int invoke(LuaState luaState) { int currentStackIndex = 1; StringBuilder stringBuilder = new StringBuilder(); while (!luaState.isNone(currentStackIndex)){ String str = luaState.toString(currentStackIndex); if (str != null){ //toString  null  non-string  non-number,  stringBuilder.append(str); } currentStackIndex++; } luaState.pushString(stringBuilder.toString()); return 1; } } 

Utilisation en lua:


 local joinedString = habrPlugin.stringJoin("this", " ", "was", " ", "concated", " ", "by", " ", "Java", "!", " ", "some", " ", "number", " : ", 42); print(joinedString) 

Exemple de renvoi de plusieurs valeurs

La classe SumFunction implémente NamedJavaFunction {
Remplacer
public String getName () {
retourner "somme";
}


 @Override public int invoke(LuaState luaState) { if (!luaState.isNumber(1) || !luaState.isNumber(2)){ luaState.pushNil(); luaState.pushString("Arguments should be numbers!"); return 2; } int firstNumber = luaState.toInteger(1); int secondNumber = luaState.toInteger(1); luaState.pushInteger(firstNumber + secondNumber); return 1; } 

}


Java Reflection - utilisation de classes Java directement dans lua


La bibliothèque jnlua possède une classe JavaReflector spéciale chargée de créer une table lua à partir d'un objet java. Ainsi, vous pouvez écrire des classes en java et les donner au code lua pour une utilisation future.


Pour ce faire, c'est assez simple:


Exemple de classe


 @SuppressWarnings({"unused"}) public class Calculator { public int sum(int number1, int number2){ return number1 + number2; } public static int someStaticMethod(){ return 4; } } 

Ajout d'une instance de cette classe à notre module


  luaState.pushJavaObject(new Calculator()); luaState.setField(-2, "calc"); luaState.pop(1); 

Utilisation à Lua:


 local calc = habrPlugin.calc print("call method of java object", calc:sum(3,4)) print("call static method of java object", calc:getClass():someStaticMethod()) 

Notez les deux-points dans l'appel de méthode de classe. Pour les méthodes statiques, vous devez également utiliser deux points.


Ensuite, j'ai remarqué une caractéristique intéressante du réflecteur: si nous ne transmettons qu'une instance de la classe à lua, alors appeler sa méthode statique est possible via getClass (). Mais après un appel via getClass (), les appels suivants seront déclenchés sur l'objet lui-même:


 print("call method of java object", calc:sum(3,4)) -- ok print("exception here", calc:someStaticMethod()) --   "com.naef.jnlua.LuaRuntimeException: no method of class plugin.habrExamplePlugin.Calculator matches 'someStaticMethod()'" print("call static method of java object", calc:getClass():someStaticMethod()) -- ok print("hmm", calc:someStaticMethod()) --    getClass         

De plus, en utilisant getClass (), nous pouvons créer de nouveaux objets directement dans lua:


 local newInstance = calc:getClass():new() 

Malheureusement, je n'ai pas pu enregistrer Calculator.class dans le champ du module en raison de "java.lang.IllegalArgumentException: type illégal" dans setField .


Créer et appeler des fonctions LUA à la volée


Cette section est apparue parce que la couronne ne fournit pas la possibilité d'accéder aux fonctions de son API directement en Java. Mais jnlua.LuaState vous permet de charger et d'exécuter du code lua arbitraire:


 class CreateDisplayTextFunction implements NamedJavaFunction{ //    API  private static String code = "local text = ...;" + "return display.newText({" + "text = text," + "x = 160," + "y = 200," + "});"; @Override public String getName() { return "createText"; } @Override public int invoke(LuaState luaState) { luaState.load(code,"CreateDisplayTextFunction code"); //    ,     luaState.pushValue(1); //        luaState.call(1, 1); //   ,      1 ,    1 return 1; } } 

N'oubliez pas d'enregistrer la fonction via LuaLoader.invoke, similaire aux exemples précédents


L'appel à lua:


 habrPlugin.createText("Hello Habr!") 

Conclusion


Ainsi, votre application Android peut utiliser toutes les capacités natives de la plateforme. Le seul inconvénient de cette solution est que vous perdez la possibilité d'utiliser Corona Simulator, ce qui ralentit le développement (le redémarrage du simulateur est presque instantané, contrairement au débogage sur un émulateur ou un appareil qui nécessite une construction + installation)


Liens utiles


  1. Code complet disponible sur github


  2. Documentation native de Corona



3) L'un des référentiels jnlua . M'a aidé à comprendre le but de certaines fonctions.

Source: https://habr.com/ru/post/fr416079/


All Articles