Corona Native for Android - using arbitrary Java code in a game written in Corona
The game engine Corona allows you to create cross-platform applications and games. But sometimes the API it provides is not enough. For such cases, there is Corona Native , which allows you to extend the functionality using native code for each platform.
The article will discuss the use of Java in projects Corona for android
To understand what is happening in the article requires a basic knowledge of Java, Lua and the engine Corona
Beginning of work
Corona and Android Studio must be installed on the computer
The folder with the installation of Corona is also a project template: Native \ Project Template \ App. Copy the entire folder and rename it to the name of your project.
Template setting
Note: I used the latest available public build for Corona - 2017.3184 . In new versions, the pattern may change, and some of the preparations from this chapter will no longer be needed.
For android, we need 2 folders inside: Corona and android
From the Corona folder, delete the Images.xcassets and LaunchScreen.storyboardc - we will not need these folders. In the main.lua file we also delete all the code - we will start the creation of the project from scratch. If you want to use an existing project, replace all the files in the Corona folder with your own
The android folder is a ready project for Android Studio, we need to open it. The first message from the studio will be "Gradle sync failed". Need to fix build.gradle:
To fix the situation, add a link to the repositories in buildscript. I also changed the version in the classpath 'com.android.tools.build:gradle' to a newer one.
// Top-level build file where you can addconfigurationoptions common toall 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
}
The next step is to change gradle-wrapper.properties . You can change it manually by replacing the gradle version in distributionUrl . Or let the studio do everything for you.
Additionally, you need to fix the build.gradle for the app module: in cleanAssets , add the line delete "$ projectDir / build / intermediates / jniLibs" , without which you have to clean the project before each launch ( taken from here )
Now the synchronization succeeded, only a few warnings remained, related to the outdated buildToolsVersion and the old syntax in the configuration. To fix them is not difficult.
Now in the studio we see 2 modules: app and plugin. It is worth renaming the application (com.mycompany.app) and plugin (plugin.library) before continuing.
In the following code, the plugin will be called plugin.habrExamplePlugin.
The default plugin contains the LuaLoader class - it will be responsible for handling calls from the lua code. There is already some code there, but let's clean it up.
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) {
return0;
}
}
Using plugin code from lua code
For binding between java and lua code in Corona Native, jnlua is used . LuaLoader implements the jnlua.JavaFunction interface, so its invoke method is available from the lua code. To make sure everything is in order, add the logging code to LuaLoader.invoke and make the require of the plugin in main.lua
@Overridepublicintinvoke(LuaState luaState){
Log.d("Corona native", "Lua Loader invoke called");
return0;
}
local habrPlugin = require("plugin.habrExamplePlugin")
print("test:", habrPlugin)
Running the application, among the logs we will see the following 2 lines:
D / Corona native: Lua Loader invoke called
I / Corona: test true
So, our application loaded the plugin, and require returns true. Now we will try to return from Java code the lua-table with functions.
To add functions to the module, use the jnlua.NamedJavaFunction interface. An example of a simple function with no arguments and no return value:
classHelloHabrFunctionimplementsNamedJavaFunction{
@Overridepublic String getName(){
return"helloHabr";
}
@Overridepublicintinvoke(LuaState L){
Log.d("Corona native", "Hello Habr!");
return0;
}
}
To register our new function in lua, use the LuaState.register method:
publicclassLuaLoaderimplementsJavaFunction{
@Overridepublicintinvoke(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 вернёт только наш модульreturn1;
}
This code requires additional explanation:
LuaState, a parameter of the invoke method, essentially represents a wrapper over a Lua virtual machine (please correct me if I put it wrong). For those familiar with using lua code from C, LuaState is the same as the lua_State pointer in C.
For those who want to delve into the wilds of working with lua, I recommend reading the manual, starting with The Application Program Interface
So, when invoke function is called, we get LuaState. It has a stack that contains the parameters passed to our function from the lua code. In this case, this is the name of the module, since the LuaLoader is executed when the require ("plugin.habrExamplePlugin") call is called.
The number returned by the invoke function indicates the number of variables from the stack, which will be returned to the lua code. In the case of the require call, this number does not affect anything, but we will use this knowledge later by creating a function that returns several values.
Adding fields to the module
In addition to functions, we can also add additional fields to the module, for example, the version:
luaState.register(libName, luaFunctions); // регистрируем наш модуль, он будет расположен на вершине стека
luaState.pushString("0.1.2"); // кладём в стек строку
luaState.setField(-2, "version"); // установка поля version у нашего модуля.
In this case, we used the index -2 to indicate that the field should be installed in our module. A negative index means that the countdown begins at the end of the stack. -1 will point to the string "0.1.2" (in lua, indices start with one).
In order not to clutter up the stack, after setting the field, I recommend calling luaState.pop (1) - throwing 1 element off the stack.
@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 вернёт только наш модуль
return0;
}
}
Function examples
Implementation:
classStringJoinFunctionimplementsNamedJavaFunction{
@Overridepublic String getName(){
return"stringJoin";
}
@Overridepublicintinvoke(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());
return1;
}
}
Use in lua:
local joinedString = habrPlugin.stringJoin("this", " ", "was", " ", "concated", " ", "by", " ", "Java", "!", " ", "some", " ", "number", " : ", 42);
print(joinedString)
class SumFunction implements NamedJavaFunction {
Override
public String getName () {
return "sum";
}
@Overridepublicintinvoke(LuaState luaState){
if (!luaState.isNumber(1) || !luaState.isNumber(2)){
luaState.pushNil();
luaState.pushString("Arguments should be numbers!");
return2;
}
int firstNumber = luaState.toInteger(1);
int secondNumber = luaState.toInteger(1);
luaState.pushInteger(firstNumber + secondNumber);
return1;
}
}
Java Reflection - using Java classes directly in lua
In the jnlua library there is a special class JavaReflector, which is responsible for creating the lua table from a java object. Thus, you can write classes in java and give them to lua code for further use.
This is quite simple:
Class example
@SuppressWarnings({"unused"})
publicclassCalculator{
publicintsum(int number1, int number2){
return number1 + number2;
}
publicstaticintsomeStaticMethod(){
return4;
}
}
Adding an instance of this class to our module
luaState.pushJavaObject(new Calculator());
luaState.setField(-2, "calc");
luaState.pop(1);
Use in 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())
Notice the colon in the class method call. For static methods you also need to use a colon.
Here I noticed an interesting feature of the reflector: if we pass only a class instance to lua, then the call of its static method is possible via getClass (). But after the call via getClass (), subsequent calls will be triggered on the object itself:
print("call method of java object", calc:sum(3,4)) -- okprint("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()) -- okprint("hmm", calc:someStaticMethod()) -- после вызова через getClass мы получили возможность работать с этим методом напрямую
Also, using getClass (), we can create new objects right in lua:
local newInstance = calc:getClass():new()
Unfortunately, I could not save Calculator.class in the module field due to "java.lang.IllegalArgumentException: illegal type" inside setField .
Creating and calling lua functions on the fly
This section appeared because the crown does not provide the ability to access functions from its api directly in Java. But jnlua.LuaState allows you to load and execute arbitrary lua code:
classCreateDisplayTextFunctionimplementsNamedJavaFunction{
// Вызываем функцию из API короныprivatestatic String code = "local text = ...;" +
"return display.newText({" +
"text = text," +
"x = 160," +
"y = 200," +
"});";
@Overridepublic String getName(){
return"createText";
}
@Overridepublicintinvoke(LuaState luaState){
luaState.load(code,"CreateDisplayTextFunction code"); // загружаем код в стек, создавая из него функцию
luaState.pushValue(1); // помещаем первый параметр функции на вершину стека
luaState.call(1, 1); // вызываем нашу функцию, указываем что она должна получить 1 параметр, а также вернуть 1return1;
}
}
Do not forget to register the function via LuaLoader.invoke, similar to the previous examples.
Call lua:
habrPlugin.createText("Hello Habr!")
Conclusion
Thus, your application on android can use all the native features of the platform. The only drawback of this solution is that you lose the ability to use the Corona Simulator, which slows down the development (restarting the simulator is almost instantaneous, unlike debugging on an emulator or device that requires build + install)
useful links
3) One of the jnlua repositories . He helped me to understand the appointment of some functions.