Corona Native для Android — использование произвольного Java кода в игре написанной на Corona

    Игровой движок Corona позволяет создавать кроссплатформенные приложения и игры. Но иногда предоставляемого им API бывает недостаточно. Для таких случаев есть Corona Native, позволяющий расширять функциональность с использованием родного кода для каждой платформы.


    В статье пойдёт речь об использовании Java в проектах Corona для android


    Для понимания происходящего в статье требуются базовые знания Java, Lua и движка Corona


    Начало работы


    На компьютере должны быть установлены Corona и Android Studio


    В папке с установкой Corona также находится шаблон проекта: Native\Project Template\App. Копируем всю папку и переименовываем в имя своего проекта.


    Настройка шаблона


    Примечание: я использовал последний доступный public build для Corona — 2017.3184. В новых версиях шаблон может измениться, и некоторые приготовления из этой главы перестанут быть нужны.


    Для android нам нужны 2 папки внутри: Corona и android


    Из папки Corona удаляем Images.xcassets и LaunchScreen.storyboardc — эти папки нам не понадобятся. В файле main.lua также удаляем весь код — мы начнём создание проекта с нуля. Если вы хотите использовать существующий проект, то замените все файлы в папке Corona на свои


    Папка android — это готовый проект для Android Studio, нам нужно открыть его. Первым же сообщением от студии будет "Gradle sync failed". Нужно исправить build.gradle:


    build gradle


    Чтобы исправить ситуацию, добавляем ссылку на repositories в buildscript. Я также изменил версию в classpath 'com.android.tools.build:gradle' на более новую.


    Код 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
    }
    

    Следующим шагом будет изменение gradle-wrapper.properties. Изменить можно вручную, заменив в distributionUrl версию gradle. Или позволить студии всё сделать за вас.


    gradle-wrapper.properties


    Дополнительно нужно поправить build.gradle для модуля app: в cleanAssets нужно добавить строчку delete "$projectDir/build/intermediates/jniLibs", без которой придётся делать clean проекта перед каждым запуском (взято отсюда)


    Теперь синхронизация удалась, осталось только несколько warnings, связанных с устаревшей buildToolsVersion и старым синтаксисом в конфигурации. Поправить их не составит труда.


    Теперь в студии мы видим 2 модуля: app и plugin. Стоит переименовать приложение (com.mycompany.app) и плагин (plugin.library), перед тем как продолжить работу.


    Далее в коде плагин будет называться plugin.habrExamplePlugin


    В плагине по умолчанию находится класс LuaLoader — именно он будет отвечать за обработку вызовов из lua кода. Там уже есть некий код, но давайте его очистим.


    Код 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;
        }   
    }

    Использование кода плагина из lua кода


    Для биндинга между java и lua кодом в Corona Native используется jnlua. LuaLoader реализует интерфейс jnlua.JavaFunction, таким образом его метод invoke доступен из lua кода. Чтобы удостовериться, что всё в порядке, добавим логгирующий код в LuaLoader.invoke и сделаем require плагина в 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)

    Запустив приложение, среди логов увидим следующие 2 строчки:


    D/Corona native: Lua Loader invoke called
    I/Corona: test true

    Итак, наше приложение загрузило плагин, а require возвращает true. Теперь попробуем вернуть из Java кода lua-таблицу с функциями.


    Для добавления функций в модуль воспользуемся интерфейсом jnlua.NamedJavaFunction. Пример простой функции без аргументов и без возвращаемого значения:


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

    Для регистрации нашей новой функции в lua используем метод 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;
        }

    Данный код требует дополнительных пояснений:


    LuaState, параметр метода invoke, по сути представляет обёртку над виртуальной машиной Lua (прошу меня скорректировать если я неверно выразился). Для тех, кто знаком с использованием lua кода из C, LuaState представляет собой то же, что и указатель lua_State в C.


    Для тех, кто хочет углубиться в дебри работы с lua, рекомендую почитать мануал, начиная с The Application Program Interface


    Итак, при вызове функции invoke мы получаем LuaState. У него есть стек, который содержит параметры, переданные в нашу функцию из lua кода. В данном случае это имя модуля, поскольку LuaLoader исполняется в момент вызова require("plugin.habrExamplePlugin").


    Возвращаемое функцией invoke число показывает количество переменных из стека, которое вернётся в lua код. В случае с вызовом require это число ни на что не влияет, но мы воспользуемся этим знанием позже, создав функцию, возвращающую несколько значений


    Добавление полей в модуль


    Помимо функций мы также можем добавить в модуль дополнительные поля, например версию:


        luaState.register(libName, luaFunctions); // регистрируем наш модуль, он будет расположен на вершине стека
        luaState.pushString("0.1.2"); // кладём в стек строку
        luaState.setField(-2, "version"); // установка поля version у нашего модуля.

    В данном случае мы воспользовались индексом -2, чтобы указать, что поле нужно установить у нашего модуля. Отрицательный индекс означает, что отсчёт начинается с конца стека. -1 будет указывать на строку "0.1.2" (в lua индексы начинаются с единицы).


    Чтобы не засорять стек, после установки поля я рекомендую вызывать luaState.pop(1) — выбрасывает из стека 1 элемент.


    Полный код LuaLoader
    @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;
        }
    }

    Примеры функций


    Пример функции, которая принимает несколько строк и конкатенирует их через String builder

    Реализация:


    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;
        }
    }

    Использование в 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";
    }


    @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 — использование Java классов напрямую в lua


    В библиотеке jnlua есть специальный класс JavaReflector, который отвечает за создание lua таблицы из java объекта. Таким образом можно писать классы на java и отдавать их в lua код для дальнейшего использования.


    Сделать это достаточно просто:


    Пример класса


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

    Добавление экземпляра этого класса к нашему модулю


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

    Использование в 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())

    Обратите внимание на двоеточие в вызове метода класса. Для статических методов также нужно использовать двоеточие.


    Тут я заметил интересную особенность рефлектора: если мы передаём в lua только экземпляр класса, то вызов его статического метода возможен через getClass(). Но после вызова через getClass() последующие вызовы буду срабатывать и на самом объекте:


    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 мы получили возможность работать с этим методом напрямую

    Также, используя getClass(), мы можем создавать новые объекты прямо в lua:


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

    К сожалению, сохранить Calculator.class в поле модуля мне не удалось из-за "java.lang.IllegalArgumentException: illegal type" внутри setField.


    Создание и вызов lua функций "на лету"


    Этот раздел появился по причине того, что корона не предоставляет возможность обратиться к функциям из своего api напрямую в Java. Но jnlua.LuaState позволяет загружать и выполнять произвольный lua код:


    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;
        }
    }

    Не забудьте зарегистрировать функцию через LuaLoader.invoke, аналогично предыдущим примерам


    Вызов в lua:


    habrPlugin.createText("Hello Habr!")

    Заключение


    Таким образом, ваше приложение на android может использовать все нативные возможности платформы. Единственный недостаток этого решения — вы лишаетесь возможности использовать Corona Simulator, что замедляет разработку (перезапуск симулятора практически мгновенен, в отличие от отладки на эмуляторе или устройстве, который требует build + install)


    Полезные ссылки


    1. Полный код доступен на гитхабе


    2. Документация по Corona Native



    3) Один из репозиториев jnlua. Помог мне разобраться в назначении некоторых функций.

    Поделиться публикацией
    Комментарии 2
      0
      спасибо! хорошо бы похожую статью про Corona Native для iOS
        0
        Там похоже — открываете `ios/App` проект, и нажимаете кнопку запуска в Xcode. Если запускаете на устройстве придется выбрать подпись.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое