Разработка плагина RuStore Billing для Defold. Часть 1: Создание Defold-проекта

9b54a95eed44b0f74e1e33dc3ccfe7f3.jpeg

Мы хотим, чтобы интеграция наших инструментов была максимально простой и понятной. Поэтому запускаем серию статей, в которых будем делиться советами по работе с RuStore Billing SDK и созданию плагинов для разных платформ и игровых движков.

Интеграция RuStore Billing SDK в игры на движке Defold может быть немного сложнее, чем нативный вариант. SDK написан на Kotlin, и взаимодействие системы скриптинга Defold на Lua с нативным SDK требует использования JNI (Java Native Interface). 

Меня зовут Роман Пельмегов, я работаю разработчиком в RuStore.

В этой статье расскажу, как мы интегрировали платежные функции RuStore в Defold. Вы узнаете, как создать плагин и подключить нативные Android-библиотеки, чтобы упростить работу с SDK.

Стек технологий

Технология

Краткое описание

Defold

Игровой движок. Для интеграции с внешними библиотеками здесь рассматривается система нативных расширений (плагинов) Defold. 

Lua

Язык для создания скриптов, встроенный в Defold и использующийся для взаимодействия с нативным SDK. Пример использования методов плагина реализуем на Lua — языке скриптинга Defold.

Kotlin

Для реализации функциональности платежей используется библиотека SDK RuStore Billing для Kotlin/Java.

C++

Язык программирования общего назначения.

JNI

Java Native Interface — интерфейс, позволяющий вызвать нативные функции SDK. Большинство методов SDK используют асинхронные вызовы и лямбда-выражения. В JNI нет прямой поддержки лямбда-выражений, поскольку лямбда-выражения являются частью высокоуровневых конструкций языков программирования, таких как Java или Kotlin, и не имеют прямого эквивалента в нативном коде C++. Однако чтобы реализовать аналогичное поведение можно использовать указатели на функции в блоках extern «C». В этом случае нам понадобится создать обёртку для методов SDK, которые будут принимать указатель на функцию обратного вызова (callback function) и вызывать её внутри лямбда-выражения.

Android

Studio

Писать обёртки над SDK методами для их вызова через JNI будем на Kotlin в Android Studio.

Gradle

Удалённая сборка Defold не поддерживает использование локально расположенных пакетов .aar, но файлы .jar работают прекрасно. Для их создания с помощью Gradle реализуем распаковку наших .aar для извлечения .jar.

Инструменты разработки​

Для создания платёжного решения в рамках гайда необходимы следующие инструменты разработки (см. таблицу ниже).

Инструмент

Описание

Defold

Скачайте движок с официального сайта в разделе Download или GitHub.

Установка не требуется — для работы нужно будет только запустить исполняемый файл (для Windows это Defold.exe).

Примечание

Для примера используется версия Defold 1.8.0. 

Android Studio

1. Скачайте интегрированную среду разработки (IDE) с официального сайта в разделе Download

2. Выполните установку согласно инструкции по установке (на англ. языке) для вашей операционной системы.

Примечание

 Для примера используется версия Android Studio Koala 2024.1.1.

JDK

Java Development Kit — набор инструментов разработчика Java. В состав Android Studio включён улучшенный набор инструментов JBR. Если по каким-то причинам при использовании JBR возникнут затруднения, скачайте JDK и установите в любое место на компьютере. Рекомендуется установить по пути по умолчанию.

Примечание

 Для примера используется JDK версии 11.0.2, набор инструментов установлен по пути C:\jdk-11.0.2.

RuStore

Console

У вас должен быть доступ к консоли разработчика RuStore. 

Вам понадобится подключить монетизацию, создать подписки или разовые покупки.

Создание Defold-проекта и настройка сборки под Android

Перед началом

  1. Создайте файл хранилища ключей. Подробные шаги описаны в документации.

  2. Создайте эмулятор Android. Как это сделать?

Когда предварительные настройки выполнены, перейдите к созданию проекта в Android Studio:

  1. Запустите исполняемый файл Defold.

  2. Выберите шаблон Mobile game и задайте имя (в примере используется billing_example).

a5480a733e23f47f13f22186a525d146.png

  1. При необходимости задайте путь к проекту в поле Location или оставьте путь по умолчанию.

Для примера мы используем следующий путь: C:\Defold\billing_example.

  1. Нажмите Create New Project, чтобы создать новый проект.

  • Отобразится окно приветствия Welcome to Defold созданного проекта.

  • Также будут созданы папки .gitignore и .gitattributes для хранения проекта в репозиториях Git.

0602818709483bbfb365e93b0bb4dc51.png

Проект создан! Перейдём к созданию и настройке сборки под Android.

Проекты на Defold собираются нелокально — это упрощает настройку проекта для сборки под Android.

  1. Откройте для редактирования как формы файл game.project, который расположен в корне проекта.

  2. В секции Platforms выберите раздел Android.

  3. В секции настроек Android выполните задайте следующие значения полей (см. таблицу ниже).

Поле

Настройка

Minimum Sdk Version

24

Target Sdk Version

33

Package

Задайте название пакета ru.rustore.billing.defold.

  1. В меню инструментов сверху выберите Project > Bundle > Android Application.

Отобразится окно Bundle Application.

  1. Укажите путь к файлам, необходимым для подписи приложения (см. таблицу ниже).

Поле

Настройка

Keystore

Укажите путь к ранее подготовленному файлу хранилища ключей key.keystore.

Keystore Password

Укажите путь к ранее подготовленному файлу пароля password.txt.

Key Password

  1. Убедитесь, что в настройке Architectures установлен флажок архитектуры 32-bit (armv7).

Проекты на Defold собираются нелокально — это упрощает настройку проекта для сборки под Android.

  1. В раскрывающемся списке Variant выберите пункт Release.

  1. Снимите флажок Generate debug symbols.

Настройки должны выглядеть следующим образом.

c2ff85e957c95232264a0739f4dabc0a.png

  1. Нажмите Create Bundle.

  2. В отобразившемся окне укажите папку, в которой будет размещена сборка проекта, и подтвердите выбор.

Если все действия были выполнены верно, по указанному пути будет создана папка сборки для Android, в нашем примере:
C:\Defold\armv7-android.

Проекты на Defold собираются нелокально — это упрощает настройку проекта для сборки под Android.

Создание расширений

Для работы платёжного плагина необходимо создать два расширения Defold (см. таблицу ниже).

Расширение

Описание

RuStoreCore

Это расширение будет содержать common-код для работы со JNI и некоторыми методами для работы с Android API. Методы этого расширения смогут использовать в том числе другие плагины на основе SDK RuStore.

RuStoreBilling

Это расширение будет содержать все необходимые настройки, исходный код, библиотеки и ресурсы, связанные с конечным плагином. Конструктор расширений автоматически распознает структуру папок и интегрирует все исходные файлы и библиотеки.

Каждое из расширений в файловой системе, согласно документации Defold, должно иметь структуру следующего вида.

my_extension/
│
├── ext.manifest
│
├── src/
│
├── include/
│
├── lib/
│   └── [platforms]
│
├── manifests/
│   └── [platforms]
│
└── res/
    └── [platforms]

Ниже представлено описание структуры расширения.

Элемент структуры

Обязательно

Описание

my_extension/

Да

Папка расширения в корне проекта (billing_example):

extension_rustore_core — для расширения RuStoreCore;

extension_rustore_billing — для расширения RuStoreBilling.

ext.manifest

Да

Файл формата YAML, который подхватывается конструктором расширений. Минимальный файл манифеста должен содержать название расширения.

src/

Да

Обязательная папка папка, должна содержать все файлы исходного кода в т.ч. нативный код.

include/

Нет

Содержит все включаемые заголовочные файлы (header files) с расширением .h.

lib/

Нет

Содержит все скомпилированные библиотеки, от которых зависит расширение. Файлы библиотек должны быть помещены в подпапки с именем платформы — в зависимости от того, какие архитектуры поддерживаются вашими библиотеками.

manifests/

Нет

Содержит дополнительные файлы, используемые в процессе сборки или комплектации.

res/

Нет

Содержит любые дополнительные ресурсы, от которых зависит расширение.

Структура расширений Defold​

RuStoreCore​

  1. В корне проекта billing_example создайте папку extension_rustore_core.

  2. Создайте файл extension_rustore_core/ext.manifest, который будет содержать только строку с именем расширения.
    extension_rustore_core/ext.manifest.

name: RuStoreCore

  1. Создайте файл extension_rustore_core/src/rustorecore.cpp и пока оставьте его пустым.

  2. Создайте папку extension_rustore_core/include и оставьте её пока пустой. В расширении RuStoreCore мы будем создавать заголовочные файлы для классов, которые будут использоваться в других расширениях SDK RuStore.

  3. Создайте папку extension_rustore_core/lib/android — в дальнейшем в неё будут помещены файлы .jar.

  4. Создайте файл extension_rustore_core/manifests/android/build.gradle.

Структура расширения RuStoreCore на текущем этапе должна иметь следующий вид.

extension_rustore_core/
│
├── include/
│
├── lib/
│   └── android/
│
├── manifests/
│   └── android
│       └── build.gradle
│
├── src/
│   └── rustorecore.cpp
│
└── ext.manifest

RuStoreBilling​

  1. В корне проекта (billing_example) создайте папку extension_rustore_billing.

  2. Создайте файл extension_rustore_billing/ext.manifest со следующим содержимым, который будет содержать только строчку с именем расширения.

name: RuStoreBilling

  1. Создайте файл extension_rustore_billing/src/rustorebilling.cpp — этот файл будет содержать JNI-вызовы нативных методов.
    Сейчас оставьте этот файл пустым — описание его содержимого будет приведено далее.

  2. Создайте папку extension_rustore_billing/lib/android — в ней впоследствии нужно будет разместить созданные в дальнейшем файлы .jar, которые будут содержать обёртку над классами из SDK RuStore billing. Эта обёртка сделает методы SDK пригодными для вызова через JNI.

  3. Создайте файл extension_rustore_billing/manifests/android/build.gradle.

Структура расширения RuStoreBilling на текущем этапе должна иметь следующий вид.

extension_rustore_billing/
│
├── lib/
│   └── android/
│
├── manifests/
│   └── android
│       └── build.gradle
│
├── src/
│  └── rustorebilling.cpp
│
└── ext.manifest

Создание точек входа в расширения​

Далее займёмся настройкой точек входа в расширения. Для этого Defold SDK использует макрос DM_DECLARE_EXTENSION.

RuStoreCore​

Реализуйте вызов DM_DECLARE_EXTENSION для расширения RuStoreCore в ранее созданном файле extension_rustore_core/src/rustorecore.cpp.

#define EXTENSION_NAME RuStoreCore
#define LIB_NAME "RuStoreCore"
#define MODULE_NAME "rustorecore"

#if defined(DM_PLATFORM_ANDROID)

static const luaL_reg Module_methods[] =
{
   {0, 0}
};

#else

static const luaL_reg Module_methods[] =
{
   {0, 0}
};

#endif

static void LuaInit(lua_State* L)
{
   int top = lua_gettop(L);
   luaL_register(L, MODULE_NAME, Module_methods);
   lua_pop(L, 1);
   assert(top == lua_gettop(L));
}

static dmExtension::Result InitializeMyExtension(dmExtension::Params* params)
{
   LuaInit(params->m_L);
   return dmExtension::RESULT_OK;
}

static dmExtension::Result UpdateMyExtension(dmExtension::Params* params)
{
   return dmExtension::RESULT_OK;
}

static dmExtension::Result FinalizeMyExtension(dmExtension::Params* params)
{
   return dmExtension::RESULT_OK;
}

DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, nullptr, nullptr, InitializeMyExtension, UpdateMyExtension, nullptr, FinalizeMyExtension)

RuStoreBilling​

Реализуйте вызов DM_DECLARE_EXTENSION для расширения RuStoreBilling в ранее созданном файле extension_rustore_billing/src/rustorebilling.cpp.

#define EXTENSION_NAME RuStoreBilling
#define LIB_NAME "RuStoreBilling"
#define MODULE_NAME "rustorebilling"

#if defined(DM_PLATFORM_ANDROID)

static const luaL_reg Module_methods[] =
{
   {0, 0}
};

#else

static const luaL_reg Module_methods[] =
{
   {0, 0}
};

#endif

static void LuaInit(lua_State* L)
{
   int top = lua_gettop(L);
   luaL_register(L, MODULE_NAME, Module_methods);
   lua_pop(L, 1);
   assert(top == lua_gettop(L));
}

static dmExtension::Result InitializeMyExtension(dmExtension::Params* params)
{
   LuaInit(params->m_L);
   return dmExtension::RESULT_OK;
}

static dmExtension::Result FinalizeMyExtension(dmExtension::Params* params)
{
   return dmExtension::RESULT_OK;
}

DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, nullptr, nullptr, InitializeMyExtension, nullptr, nullptr, FinalizeMyExtension)

JNI-вызовы и обратные вызовы (callback)​

Создание метода отображения тоста

Для примера рассмотрим создание простого всплывающего уведомления (тоста) в рамках создаваемого приложения. Отображение тоста можно реализовать с помощью вызова Java метода через JNI или с помощью обратного вызова (callback).

  1. Создайте файл extension_rustore_core/src/Example.java со следующим содержимым: .extension_rustore_core/src/Example.java.

package ru.rustore.defold.example;

import android.app.Activity;import android.widget.Toast;public class Example {    public static void showToast(Activity activity, String message) {        activity.runOnUiThread(() -> Toast.makeText(activity, message, Toast.LENGTH_LONG).show());    }}
  1. Для вызова метода showToast в файле extension_rustore_core/src/rustorecore.cpp создайте метод, реализующий JNI-запросы.

    Примечание.
    Код нужно вставить в секцию #if defined(DM_PLATFORM_ANDROID), чтобы код выполнялся только на устройствах Android, и перед массивом Module_methods.

    См. пример фаргмента кода До и После ниже.

    До:

//...

static const luaL_reg Module_methods[] =

{

       {0, 0}

};

#else

//...

После:

//...

static const luaL_reg Module_methods[] =

{

       {"show_toast", ShowToast},

       {0, 0}

};

#else

//...

Созданным методом можно воспользоваться в Lua через имя модуля, указанное в MODULE_NAME при вызове luaL_register.

  1. В файле extension_rustore_billing/src/rustorebilling.cpp вставьте строку #include перед секцией #if defined(DM_PLATFORM_ANDROID) (см. пример кода ниже).

До:

#define EXTENSION_NAME RuStoreBilling

#define LIB_NAME "RuStoreBilling"

#define MODULE_NAME "rustorebilling"

#if defined(DM_PLATFORM_ANDROID)

static const luaL_reg Module_methods[] =

{

   {0, 0}

};

#else

//...

После:

#define EXTENSION_NAME RuStoreBilling
#define LIB_NAME "RuStoreBilling"
#define MODULE_NAME "rustorebilling"

#include 

#if defined(DM_PLATFORM_ANDROID)

static const luaL_reg Module_methods[] =
{
   {0, 0}
};

#else
//...
  1. Откройте файл main/main.script и добавьте в метод on_input вызова нашего метода (см. пример кода ниже).

    До:

    function init(self)
    
        msg.post(".", "acquire_input_focus")
    
        msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })
    
    end
    
    function on_input(self, action_id, action)
    
        if action_id == hash("touch") and action.pressed then
    
            print("Touch!")
    
        end
    
    end

После:

function init(self)

    msg.post(".", "acquire_input_focus")

    msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })

end

function on_input(self, action_id, action)

    if action_id == hash("touch") and action.pressed then

        rustorecore.show_toast("Hello JNI")

    end

end

6. Выполните сборку:

ИЛИ

Готово! Теперь каждый тап по экрану будет запускать тост (см. изображение ниже).

Обратный вызов (callback)

Обратный вызов (callback) представляет собой немного более сложную задачу. Для примера напишем обратный вызов, который срабатывает после вызова ShowToast и делает запись в Logcat средствами Defold.

  1. Модифицируйте класс Example в файле extension_rustore_core/src/Example.java, для этого:

  • добавьте приватный метод NativeCallback с ключевым словом native;

  • используйте метод для получения текущей даты Date и вызов NativeCallback.

После всех доработок Java-код примет следующий вид.


До:

package ru.rustore.defold.example;

import android.app.Activity;

import android.widget.Toast;

public class Example {

    public static void showToast(Activity activity, String message) {

        activity.runOnUiThread(() -> Toast.makeText(activity, message, Toast.LENGTH_LONG).show());

    }

}

После:

package ru.rustore.defold.example;

import android.app.Activity;

import android.widget.Toast;

import java.util.Date;

public class Example {

    private static native void NativeCallback(String value);

    public static void showToast(Activity activity, String message) {

        activity.runOnUiThread(() -> Toast.makeText(activity, message, Toast.LENGTH_LONG).show());

        Date currentDate = new Date();

        NativeCallback(currentDate.toString() + ": " + message);

    }

}
  1. JNI требует использования соглашения о вызовах C для взаимодействия между кодом на Java и кодом на C++ — для этого в файле extension_rustore_core/src/rustorecore.cpp объявите секцию extern «C» и внутри неё напишите функцию согласно соглашениям о вызовах JNI.

До:

//...

#if defined(DM_PLATFORM_ANDROID)

#include 

#include 

static int ShowToast(lua_State* L)

//...

После:

//...

#if defined(DM_PLATFORM_ANDROID)

#include 

#include 

#include 

extern "C"

{

    JNIEXPORT void JNICALL Java_ru_rustore_defold_example_Example_NativeCallback(JNIEnv* env, jobject obj, jstring jvalue)

    {

        const char* value = env>GetStringUTFChars(jvalue, 0);        

        __android_log_print(ANDROID_LOG_INFO, "Example", value);

        env>ReleaseStringUTFChars(jvalue, value);

    }

}

static int ShowToast(lua_State* L)

//...
  1. Выполните сборку:

ИЛИ

Итоги

Итак, мы создали простое приложение в Defold. Теперь у нас есть базовое понимание того, как работать с JNI и нативным кодом. В следующей части статьи мы подключим библиотеку RuStore Billing SDK для реализации внутриигровых покупок и создадим демо-приложение, чтобы протестировать все функции плагина.

© Habrahabr.ru