Разработка плагина RuStore Billing для Defold. Часть 1: Создание Defold-проекта
Мы хотим, чтобы интеграция наших инструментов была максимально простой и понятной. Поэтому запускаем серию статей, в которых будем делиться советами по работе с 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
Перед началом
Создайте файл хранилища ключей. Подробные шаги описаны в документации.
Создайте эмулятор Android. Как это сделать?
Когда предварительные настройки выполнены, перейдите к созданию проекта в Android Studio:
Запустите исполняемый файл Defold.
Выберите шаблон Mobile game и задайте имя (в примере используется billing_example).
При необходимости задайте путь к проекту в поле Location или оставьте путь по умолчанию.
Для примера мы используем следующий путь: C:\Defold\billing_example.
Нажмите Create New Project, чтобы создать новый проект.
Отобразится окно приветствия Welcome to Defold созданного проекта.
Также будут созданы папки .gitignore и .gitattributes для хранения проекта в репозиториях Git.
Проект создан! Перейдём к созданию и настройке сборки под Android.
Проекты на Defold собираются нелокально — это упрощает настройку проекта для сборки под Android.
Откройте для редактирования как формы файл game.project, который расположен в корне проекта.
В секции Platforms выберите раздел Android.
В секции настроек Android выполните задайте следующие значения полей (см. таблицу ниже).
Поле | Настройка |
Minimum Sdk Version | 24 |
Target Sdk Version | 33 |
Package | Задайте название пакета ru.rustore.billing.defold. |
В меню инструментов сверху выберите Project > Bundle > Android Application.
Отобразится окно Bundle Application.
Укажите путь к файлам, необходимым для подписи приложения (см. таблицу ниже).
Поле | Настройка |
Keystore | Укажите путь к ранее подготовленному файлу хранилища ключей key.keystore. |
Keystore Password | Укажите путь к ранее подготовленному файлу пароля password.txt. |
Key Password |
Убедитесь, что в настройке Architectures установлен флажок архитектуры 32-bit (armv7).
Проекты на Defold собираются нелокально — это упрощает настройку проекта для сборки под Android.
В раскрывающемся списке Variant выберите пункт Release.
Снимите флажок Generate debug symbols.
Настройки должны выглядеть следующим образом.
Нажмите Create Bundle.
В отобразившемся окне укажите папку, в которой будет размещена сборка проекта, и подтвердите выбор.
Если все действия были выполнены верно, по указанному пути будет создана папка сборки для 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):
|
ext.manifest | Да | Файл формата YAML, который подхватывается конструктором расширений. Минимальный файл манифеста должен содержать название расширения. |
src/ | Да | Обязательная папка папка, должна содержать все файлы исходного кода в т.ч. нативный код. |
include/ | Нет | Содержит все включаемые заголовочные файлы (header files) с расширением .h. |
lib/ | Нет | Содержит все скомпилированные библиотеки, от которых зависит расширение. Файлы библиотек должны быть помещены в подпапки с именем платформы — в зависимости от того, какие архитектуры поддерживаются вашими библиотеками. |
manifests/ | Нет | Содержит дополнительные файлы, используемые в процессе сборки или комплектации. |
res/ | Нет | Содержит любые дополнительные ресурсы, от которых зависит расширение. |
Структура расширений Defold
RuStoreCore
В корне проекта billing_example создайте папку extension_rustore_core.
Создайте файл
extension_rustore_core/ext.manifest
, который будет содержать только строку с именем расширения.
extension_rustore_core/ext.manifest.
name: RuStoreCore
Создайте файл extension_rustore_core/src/rustorecore.cpp и пока оставьте его пустым.
Создайте папку extension_rustore_core/include и оставьте её пока пустой. В расширении RuStoreCore мы будем создавать заголовочные файлы для классов, которые будут использоваться в других расширениях SDK RuStore.
Создайте папку extension_rustore_core/lib/android — в дальнейшем в неё будут помещены файлы
.jar.
Создайте файл extension_rustore_core/manifests/android/build.gradle.
Структура расширения RuStoreCore на текущем этапе должна иметь следующий вид.
extension_rustore_core/
│
├── include/
│
├── lib/
│ └── android/
│
├── manifests/
│ └── android
│ └── build.gradle
│
├── src/
│ └── rustorecore.cpp
│
└── ext.manifest
RuStoreBilling
В корне проекта (
billing_example
) создайте папку extension_rustore_billing.Создайте файл
extension_rustore_billing/ext.manifest
со следующим содержимым, который будет содержать только строчку с именем расширения.
name: RuStoreBilling
Создайте файл extension_rustore_billing/src/rustorebilling.cpp — этот файл будет содержать JNI-вызовы нативных методов.
Сейчас оставьте этот файл пустым — описание его содержимого будет приведено далее.Создайте папку extension_rustore_billing/lib/android — в ней впоследствии нужно будет разместить созданные в дальнейшем файлы
.jar
, которые будут содержать обёртку над классами из SDK RuStore billing. Эта обёртка сделает методы SDK пригодными для вызова через JNI.Создайте файл 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).
Создайте файл 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()); }}
Для вызова метода 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
.
В файле
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
//...
Откройте файл
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.
Модифицируйте класс
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);
}
}
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)
//...
Выполните сборку:
ИЛИ
Итоги
Итак, мы создали простое приложение в Defold. Теперь у нас есть базовое понимание того, как работать с JNI и нативным кодом. В следующей части статьи мы подключим библиотеку RuStore Billing SDK для реализации внутриигровых покупок и создадим демо-приложение, чтобы протестировать все функции плагина.