Перейти к основному содержимому

Платёжный плагин для Defold

Введение

Монетизация с помощью встроенных покупок распространена в мобильных играх, которые предлагают премиум-подписки, продажу внутриигровых валют, предметов и т. п.

Чтобы проводить платежи через платёжную систему RuStore непосредственно из интерфейсов мобильных приложений без перенаправлений к браузерам для открытия платёжных форм, можно использовать SDK платежей RuStore (SDK RuStore Billing).

примечание

В настоящем руководстве SDK платежей и SDK RuStore Billing означают одно и то же.

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

Условия воспроизведения

Приведённые в настоящем руководстве действия выполнялись в следующем окружении.

Особенности реализации

SDK RuStore Billing предоставляет эффективный способ работы с платежами, однако он может быть сложен в использовании в игровых движках из-за того, что написан на языке Kotlin.

Взаимодействие системы скриптинга Defold на языке Lua с нативным SDK предполагает использование JNI (Java Native Interface), что требует громоздкой реализации вызовов и обратных вызовов (callback) JNI на C++.

Здесь мы рассмотрим удобный и гибкий способа интеграции платежных функций RuStore в приложения на игровом движке Defold на платформе Android.

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

  • Создание плагина для Defold.
  • Подключение нативных Android-библиотек SDK RuStore Billing.
  • Создание JNI-обертки над методами SDK и их вызов из Lua.
  • Создание и настройка демо-приложение для тестирования всех методов плагина.

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

ТехнологияКраткое описание
DefoldИгровой движок. Для интеграции с внешними библиотеками в настоящей инструкции рассматривается система нативных расширений (плагинов) Defold. Шаги по созданию плагинов описаны в официальной документации Defold, доступна русскоязычная версия документации.
LuaЯзык для создания скриптов, встроенный в Defold и использующийся для взаимодействия с нативным SDK. Пример использования методов плагина реализуем на Lua – языке скриптинга Defold.
KotlinДля реализации функциональности платежей используется библиотека SDK RuStore Billing для Kotlin/Java.
C++Язык программирования общего назначения.
JNIJava Native Interface — интерфейс, позволяющий вызвать нативные функции SDK. Методы Defold для работы с нативом Android описаны на странице Sdk android api documentation официального сайта. Большинство методов SDK используют асинхронные вызовы и лямбда-выражения. В JNI нет прямой поддержки лямбда-выражений, поскольку лямбда-выражения являются частью высокоуровневых конструкций языков программирования, таких как Java или Kotlin, и не имеют прямого эквивалента в нативном коде C++. Однако чтобы реализовать аналогичное поведение можно использовать указатели на функции в блоках extern "C". В этом случае нам понадобится создать обёртку для методов SDK, которые будут принимать указатель на функцию обратного вызова (callback function) и вызывать её внутри лямбда-выражения.
Android StudioПисать обёртки над SDK методами для их вызова через JNI будем на Kotlin в Android Studio.
GradleУдалённая сборка Defold не поддерживает использование локально расположенных пакетов .aar, но файлы .jar работают прекрасно. Для их создания с помощью Gradle реализуем распаковку наших .aar для извлечения .jar.
RuStore ConsoleКонсоль разработчика RuStore. Для тестирования работы плагина необходимо зарегистрироваться в RuStore Console и создать запись о приложении и тестовых продуктах доступных для покупки.

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

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

ИнструментОписание

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.

Предварительные действия

Создание файла хранилища ключей

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

Для выполнения подписи понадобится файл хранилища ключей в формате .keystore.

Для создания файла хранилища ключей в рамках настоящего руководства используется утилита командной строки keytool из состава Android Studio.

Чтобы создать файл хранилища ключей, выполните следующие действия.

  1. Откройте консоль (в Windows с помощью команды cmd).

  2. Перейдите в папку с утилитой keytool.

    Для Windows

    Если программное обеспечение Android Studio установлено в папку по умолчанию, что утилита keytool расположена по следующему пути:

    c:\Program Files\Android\Android Studio\jbr\bin\keytool.exe.

  3. Выполните следующую команду.

    keytool -genkeypair -v -keystore key.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias key-alias
  4. Вы процессе выполнения команды заполните необходимые поля (см. таблицу ниже).

ПолеОписание
Enter keystore password:

Введите пароль для защиты файла хранилища ключей.

Re-enter new password:

Повторите введённый ранее пароль.

подсказка

Сохраните пароль в простом файле .txt (например: password.txt) — в этом файле должна содержаться только одна строка сохранённого пароля. Для целей настоящего руководства будет использоваться путь c:\Defold\keystore\password.txt.

Внимание!

Это небезопасный способ хранения пароля. В настоящем руководстве он используется для примера. При реализации полноценного платёжного решения руководствуйтесь рекомендациями безопасности.

What is your first and last name?
[Unknown]:

Введите имя и фамилию, например: John Doe.

What is the name of your organizational unit?
[Unknown]:

Введите название организационного подразделения, например: DEV.

What is the name of your organization?
[Unknown]:

Введите название организации, например: ORG.

What is the name of your City or Locality?
[Unknown]:

Введите название или обозначение города или населённого пункта, например: MSK.

What is the name of your State or Province?
[Unknown]:

Введите название или обозначение региона, например: MSK.

What is the two-letter country code for this unit?
[Unknown]:

Введите двухбуквенный код страны, например: RU.

После заполнения полей вам будет предложено проверить свой ввод.

Is CN=John Doe, OU=DEV, O=ORG, L=MSK, ST=MSK, C=RU correct?
[no]:
  1. Введите yes и подтвердите ввод.
    В случае успешного создания файла хранилища ключей отобразится следующее сообщение.
    Generating 2 048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10 000 days
    for: CN=John Doe, OU=DEV, O=ORG, L=MSK, ST=MSK, C=RU
    [Storing key.keystore]
  2. Сохраните созданный файл хранилища ключей key.keystore в безопасном месте — он потребуется для настройки сборки Defold-проекта под Android.
    Для целей настоящего руководства будет использоваться путь c:\Defold\keystore\key.keystore.
Внимание!

Если вы потеряете этот файл, то потеряете возможность обновлять загруженные в магазин приложения.

Вы создали файл хранилища ключей, и у вас есть два сохранённых файла (см. таблицу ниже).

ФайлОписание
key.keystoreФайл ключевого хранилища. В настоящем руководстве для примера используется путь
c:\Defold\keystore\key.keystore.
password.txtФайл пароля ключевого хранилища и закрытого ключа. В настоящем руководстве для примера используется путь
c:\Defold\keystore\password.txt.

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

Создание эмулятора Android

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

ПараметрЗначение
Архитектураx86
Версия Android11 (максимальная для архитектуры x86)
OpenGL ES renderer
(Extended Controls)
Desktop native OpenGL — для обеспечения наилучшего быстродействия.
OpenGL ES API level
(Extended Controls)
Renderer maximum (up to OpenGL ES 3.1) — для обеспечения наилучшего быстродействия.
подсказка

Если вы не знаете, как создать виртуальный эмулятор с указанными параметрами, ниже представлена пошаговая процедура (пропустить).

  1. В Android Studio откройте менеджер виртуальных устройств — Virtual Device Manager.


    Virtual Device Manager

  2. В окне Device Manager нажмите значок .
    Отобразится окно добавления виртуального устройства.


    Окно добавления виртуального устройства

  3. В списке Category слева выберите Phone.

  4. В списке в центральной части окна выберите профиль устройства.

    примечание

    В настоящей документации для примера будет использоваться Medium Phone.

  5. Нажмите Next.
    Отобразится окно выбора образа системы.


    Выбор образа системы

  6. Выберите нужную систему.

    предупреждение

    Архитектура эмулятора должна быть x86.

    примечание

    Максимальная версия Android доступная для x86-эмуляторов — 11.

    В настоящей документации для примера используется система с именем релиза Release name: R.

  7. Нажмите на значке загрузки напротив выбранной системы.
    Отобразится окно загрузки и установки образа системы (SDK Component Installer).


    Окно загрузки и установки образа системы

    По завершении установки окно примет следующий вид.


    Выбор образа системы

  8. Нажмите Finish.

  9. В окне выбора образа системы (System Image) нажмите Next.
    Отобразится окно подтверждения конфигурации виртуального устройства.


    Выбор образа системы

  10. Нажмите Finish.
    Установленный образ появится в списке Device Manager основного окна Android Studio.
    По завершении установки окно примет следующий вид.


    Виртуальное устройство установлено

    к сведению

    Дальнейшая настройка эмулятора устройства Android нужна для обеспечения наилучшего быстродействия.

  11. В секции Device Manager Нажмите значок Start напротив установленного виртуального устройства.
    Отобразится окно виртуального эмулятора Android.


    Виртуальный эмулятор Android

  12. Нажмите на значок .
    Отобразится окно расширенных функций контроля (Extended Controls).


    Окно Extended Controls

  13. В левом меню перейдите в раздел Settings.

  14. В разделе Settings выберите вкладку Advanced.
    Окно примет следующий вид.


    Окно Extended Controls

  15. Задайте значения полей, руководствуясь таблицей ниже.

    ПолеЗначение
    OpenGL ES rendererDesktop native OpenGL
    OpenGL ES API levelRenderer maximum (up to OpenGL ES 3.1)
  16. Закройте окно Extended Controls.

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

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

  2. Выберите шаблон Mobile game и назовите его billing_example (см. изображение ниже).


    Создание проекта Defold

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

    к сведению

    В настоящем руководстве для примера будет использоваться следующий путь:
    C:\Defold\billing_example.

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

    • Отобразится окно приветствия Welcome to Defold созданного проекта.
    • Также будут созданы папки .gitignore и .gitattributes для хранения проекта в репозиториях Git.
      (см. изображение ниже).


    Окно приветствия проекта Defold

    Проект создан. Следом настройте и создайте сборку под Android (см. далее).

    подсказка

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

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


    Открыть как форму

  6. В секции Platforms выберите раздел Android (см. изображение ниже).


    Открыть как форму

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

    ПолеНастройка
    Minimum Sdk Version24
    Target Sdk Version33
    PackageЗадайте название пакета ru.rustore.billing.defold.

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


    Настройка проекта Defold под Android

  8. В меню инструментов сверху выберите Project > Bundle > Android Application…, как показано на изображении ниже.


    Project > Bundle > Android Application…


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


    Bundle Application

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

    ПолеОписание
    Keystore

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

    Keystore Password

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

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

    примечание

    Чтобы приложение могло быть запущено на архитектурах 32-bit (armv7) и 64-bit (arm64), отметьте оба флажка.

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

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


    Настройки Bundle Application

  13. Нажмите Create Bundle

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

к сведению

В настоящем руководстве для примера будет использоваться папка
C:\Defold.

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

примечание

В папку armv7-android попадают сборки как armv7, так и arm64.

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

к сведению

В настоящей инструкции приведены минимально необходимые настройки.

Для работы платёжного плагина необходимо создать два расширения 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
  3. Создайте файл extension_rustore_core/src/rustorecore.cpp и пока оставьте его пустым.
  4. Создайте папку extension_rustore_core/include и оставьте её пока пустой. В расширении RuStoreCore мы будем создавать заголовочные файлы для классов, которые будут использоваться в других расширениях SDK RuStore.
  5. Создайте папку extension_rustore_core/lib/android — в дальнейшем в неё будут помещены файлы .jar.
  6. Создайте файл 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 со следующим содержимым, который будет содержать только строчку с именем расширения.
    extension_rustore_billing/ext.manifest
    name: RuStoreBilling
  3. Создайте файл extension_rustore_billing/src/rustorebilling.cpp — этот файл будет содержать JNI-вызовы нативных методов.
    Сейчас оставьте этот файл пустым — описание его содержимого будет приведено далее.
  4. Создайте папку extension_rustore_billing/lib/android — в ней впоследствии нужно будет разместить созданные в дальнейшем файлы .jar, которые будут содержать обёртку над классами из SDK RuStore billing. Эта обёртка сделает методы SDK пригодными для вызова через JNI.
  5. Создайте файл 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.

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.

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)

Для примера рассмотрим создание всплывающего окна простого уведомления (тоста) в рамках создаваемого приложения. Отображение тоста можно реализовать с помощью вызова метода или с помощью обратного вызова (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());
    }
    }
  2. Для вызова метода showToast в файле extension_rustore_core/src/rustorecore.cpp создайте метод, реализующий JNI-запросы.

    примечание

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

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

    extension_rustore_core/src/rustorecore.cpp
    //...
    #if defined(DM_PLATFORM_ANDROID)

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

    #else
    //...
  3. В этом же файле (extension_rustore_core/src/rustorecore.cpp) в массиве Module_methods добавьте псевдоним метода show_toast для вызова через Lua (см. пример фрагмента кода до и после).

    extension_rustore_core/src/rustorecore.cpp
    //...
    static const luaL_reg Module_methods[] =
    {
    {0, 0}
    };

    #else
    //...
    примечание

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

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

    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
    //...
  5. Откройте файл main/main.script и добавьте в метод on_input вызова нашего метода (см. пример кода ниже).

    main.script
    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
  6. Выполните сборку:

    • Project > Bundle > Android Application > Create Bindle.
      ИЛИ
    • Project > Rebundle (если вы уже делали сборку во время сеанса работы с Defold).

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


Запуск примера вызовов через JNI

подсказка

Чтобы проверить работу сборки:

  1. запустите созданный ранее эмулятор Android;
  2. перетащите в него APK-файл из папки сборки;
  3. запустите приложение и в его интерфейсе сэмулируйте тап на экране смартфона.

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

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

к сведению

Logcat - инструмент, который позволяет получить доступ к системным сообщениям Android.

  1. Модифицируйте класс Example в файле extension_rustore_core/src/Example.java, для этого:
    1. добавьте приватный метод NativeCallback с ключевым словом native;
    2. используйте метод для получения текущей даты Date и вызов NativeCallback.
      После всех доработок 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());
    }
    }
  2. JNI требует использования соглашения о вызовах C для взаимодействия между кодом на Java и кодом на C++ — для этого в файле extension_rustore_core/src/rustorecore.cpp объявите секцию extern "C" и внутри неё напишите функцию согласно соглашениям о вызовах JNI.
    extension_rustore_core/src/rustorecore.cpp
    //...
    #if defined(DM_PLATFORM_ANDROID)

    #include <dmsdk/sdk.h>
    #include <dmsdk/dlib/android.h>

    static int ShowToast(lua_State* L)
    //...

    Описание кода представлено в таблице ниже.
    Элемент кодаОписание
    extern "C" { }Объявление, которое указывает компилятору C++ на использование соглашения о вызовах C для функции, чтобы функция могла быть вызвана из кода на Java.
    JNIEXPORTМакрос, определённый в заголовочных файлах JNI, который обозначает экспорт функции для использования из других библиотек. Этот макрос обеспечивает правильное экспортирование функции для работы с JNI.
    JNICALLСпецификатор, который указывает на использование соглашения о вызовах Java для функции.
    Java_ru_rustore_defold_example_Example_NativeCallbackСигнатура JNI функции, включающая:
    • id пакета ru_rustore_defold_example;
    • имя класса Example;
    • название метода NativeCallback.
    JNIEnv* envУказатель на структуру JNIEnv, который предоставляет доступ к функциям JNI. Обязательный параметр функции JNI.
    jobject objОбъект, который вызывает метод NativeCallback. Обязательный параметр функции JNI.
    jstring jvalueПараметр, передаваемый со стороны Java-кода.
    GetStringUTFCharsМетод выполняет преобразование объекта jstring в const char*.
    __android_log_printМетод из файла android/log.h, позволяет выводить сообщения в logcat.
    ReleaseStringUTFCharsЭтот метод выполняет необходимое удаление объектов строк.
  3. Выполните сборку:
    • Project > Bundle > Android Application > Create Bindle.
      ИЛИ
    • Project > Rebundle (если вы уже делали сборку во время сеанса работы с Defold). Каждый показ тоста теперь сопровождается записью в Logcat вида: Tue May 14 16:51:16 GMT 2024: Hello JNI.

Создание проекта в Android Studio

Весь необходимый нам нативный код можно написать непосредственно в редакторе Defold. Но работать в специальных IDE намного продуктивнее да и просто приятнее. Потому создадим проект Android и настроим его на генерацию файлов .jar, понятных Defold.

  1. Запустите Android Studio и создайте новый проект (кнопка New Project).
    Отобразится следующее окно.


    Создание нового проекта Android

  2. В разделе Phone and Tablet выберите шаблон No Activity, после чего нажмите Next.
    Отобразится окно New Project.


    Создание нового проекта Android

  3. Заполните поля, руководствуясь таблицей ниже.
    ПолеОписание
    NameЗадайте название проекта: extension_libraries.
    Package nameЗадайте название пакета: ru.rustore.defold.billing.
    Save locaitonЗадайте путь сохранения проекта. В настоящей инструкции будет использоваться путь
    C:\Projects\extension_libraries.
    LanguageВыберите язык программирования Kotlin.
    Minimum SDKВыберите SDK – API 24: Android ("Nougat"; Android 7.0).
    warning

    Если в вашей версии Android Studio присутствует настройка Build Configuration Language, выберите пункт Groovy DSL (build.gradle).

  4. Нажмите Finish.
    Проект отобразится в окне Android Studio.


    Проект extension_libraries создан

  5. для удобства дальнейшей работы на вкладке Project слева вверху переключитесь на отображение структуры Project вместо Android (см. изображение ниже).


    Выбор формата отображение проекта

    Структура проекта выглядит следующим образом.


    Отображение структуры проекта

Переходите к настройке проекта.

Первичная настройка проекта

Чтобы настроить проект, выполните следующие действия.

  1. В корне проекта (extension_libraries) найдите файл settings.gradle:
  2. Удалите из файла все данные, кроме строки rootProject.name = "extension_libraries" (см. примеры кода До и После).
    settings.gradle
    pluginManagement {
    repositories {
    google {
    content {
    includeGroupByRegex("com\\.android.*")
    includeGroupByRegex("com\\.google.*")
    includeGroupByRegex("androidx.*")
    }
    }
    mavenCentral()
    gradlePluginPortal()
    }
    }
    dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
    google()
    mavenCentral()
    }
    }

    rootProject.name = "extension_libraries"
    include ':app'
  3. В корне проекта (extension_libraries) найдите файл build.gradle и замените содержимое файла следующей конфигурацией.
    warning

    Обратите внимание, что переменная rustore_example_folder должна содержать путь до проекта Defold. В нашем примере это
    c:\Defold\billing_example\. Для обратной наклонной черты нужно использовать экранирующий символ (см. пример До и После ниже.)

    build.gradle
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.jetbrains.kotlin.android) apply false
    }
  4. Справа вверху нажимаем кнопку Sync Now (см. изображение ниже).


    Синхронизация проекта

    подсказка

    Могут появиться предупреждения (warning). Здесь и далее, если предупреждения не препятствуют дальнейшей работе, игнорируйте их.

  5. После обновления проекта удалите папку app из контекстного меню (см. изображение ниже).


    Удаление папки app

  6. В меню инструментов Android Studio выберите File > Project Structure (см. изображение ниже).


    Отображение структуры проекта

  7. В отобразившемся окне Project Structure убедитесь, что в разделе Project выставлены следующие настройки (см. таблицу и изображение ниже) Перед созданием модулей проверьте, чтоб проект имеет следующие настройки (см. таблицу ниже)
    НастройкаОписание
    Android Gradle Plugin Version4.1.1
    Gradle Version6.7.1


    Структура проекта

  8. Выполните одно из следующий действий:
    • если настройки соответствуют указанным выше, переходите к созданию модулей.
    • если настройка Gradle Version отличается от 6.7.1, выберите эту версию в списке, после чего нажмите OK для подтверждения и продолжите процедуру.
      После выбора версии Gradle Version 6.7.1 в Android Studio может отобразиться ошибка синхронизации.


    Ошибка синхронизации проекта

    подсказка

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

  9. В панели инструментов Android Studio выберите File > Settings.


    Отображение настроек проекта

  10. В открывшемся окне Settings перейдите в раздел Build, Extension, Deployment > Build Tools > Gradle.


    Окно настроек проекта

  11. В списке Gradle JDK выберите JDK версии 11 (см. изображение ниже).


    Выбор версии JDK 11

    подсказка

    Если JDK 11 в списке отсутствует, нажмите под списком Gradle JDK и укажите путь к загруженной ранее версии JDK.

  12. Нажмите OK, чтобы сохранить изменения.
  13. Синхронизируйте проект.
    Если все действия были выполнены верно, будет выполнена синхронизация.


    Синхронизация успешна

Переходите к созданию модулей.

Создание модулей проекта

RuStoreDefoldCore

В Android Studio создайте новый модуль RuStoreDefoldCore. Для этого выполните следующие действия.

  1. В панели инструментов Android Studio выберите File > New > New Module.
  2. В открывшемся окне Create New Module в списке Templates выберите шаблон Android Library и выполните настройки, как показано в таблице ниже.
    ПолеОписание
    Module nameЗадайте значение RuStoreDefoldCore.
    Package nameЗадайте значение ru.rustore.defold.core.
    LanguageВыберите Kotlin.
    Bytecode LevelВыберите 8 (slower build).
    Minimum SDKВыберите API 24 ("Nougat"; Android 7.0).
    warning

    Если в вашей версии Android Studio присутствует настройка Build Configuration Language, выберите пункт Groovy DSL (build.gradle).

    Настройки должны быть как на изображении ниже.


    Создание модуля RuStoreDefoldCore

  3. Нажмите Finish.

Модуль RuStoreDefoldCore создан. Переходите к настройке модуля.

build.gradle

Выполните следующие изменения в файле RuStoreDefoldCore/build.gradle

  1. в разделе android найдите и удалите следующую строку namespace 'ru.rustore.defold.core'.

    RuStoreDefoldCore/build.gradle
    //...
    android {
    namespace 'ru.rustore.defold.core'
    compileSdk 34

    defaultConfig {
    //...
  2. Замените содержимое раздела dependencies, подключив следующие зависимости (см. пример До и После).

    RuStoreDefoldCore/build.gradle
    //..
    dependencies {

    implementation libs.appcompat.v7
    testImplementation libs.junit
    androidTestImplementation libs.runner
    androidTestImplementation libs.espresso.core
    }
  3. В конец файла вставьте следующий код для извлечения и перемещения .jar-файла.

    RuStoreDefoldCore/build.gradle
    //..
    dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "com.google.code.gson:gson:2.10.1"
    implementation "androidx.fragment:fragment:1.3.0"
    }

AndroidManifest.xml

  1. В папке RuStoreDefoldCore/src/main найдите файл AndroidManifest.xml.
  2. В тег <manifest> добавьте атрибут package="ru.rustore.defold.core">, как показано ниже.
    RuStoreDefoldCore/src/main
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">

    </manifest>

RuStoreDefoldBilling

В Android Studio создайте новый модуль RuStoreDefoldBilling. Для этого выполните следующие действия.

  1. В панели инструментов Android Studio выберите File > New > New Module.
  2. В открывшемся окне Create New Module в списке Templates выберите шаблон Android Library и выполните настройки, как показано в таблице ниже.
    ПолеОписание
    Module nameЗадайте значение RuStoreDefoldBilling.
    Package nameЗадайте значение ru.rustore.defold.billing.
    LanguageВыберите Kotlin.
    Bytecode LevelВыберите 8 (slower build).
    Minimum SDKВыберите API 24 ("Nougat"; Android 7.0).
    warning

    Если в вашей версии Android Studio присутствует настройка Build Configuration Language, выберите пункт Groovy DSL (build.gradle).

    Настройки должны быть как на изображении ниже.


    Создание модуля RuStoreDefoldBilling

  3. Нажмите Finish.

Модуль RuStoreDefoldBilling создан. Переходите к настройке модуля.

build.gradle

Выполните следующие изменения в файле RuStoreDefoldBilling/build.gradle.

  1. в разделе android найдите и удалите строку namespace 'ru.rustore.defold.billing'.
    RuStoreDefoldBilling/build.gradle
    //...
    android {
    namespace 'ru.rustore.defold.billing'
    compileSdk 34

    defaultConfig {
    //...
  2. Замените содержимое раздела dependencies, подключив следующие зависимости (см. пример До и После).
    RuStoreDefoldBilling/build.gradle
    dependencies {

    implementation libs.appcompat.v7
    testImplementation libs.junit
    androidTestImplementation libs.runner
    androidTestImplementation libs.espresso.core
    }
  3. В конец файла вставьте следующий код — он позволит распаковать .aar-пакет для извлечения .jar-файла и поместит его в наш проект Defold (см. сравнение До и После).
    RuStoreDefoldBilling/build.gradle
    //...
    dependencies {

    implementation "ru.rustore.sdk:billingclient:5.1.1"
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "com.google.code.gson:gson:2.10.1"
    implementation "androidx.fragment:fragment:1.3.0"
    implementation project(path: ':RuStoreDefoldBilling')
    }

AndroidManifest.xml

  1. В папке RuStoreDefoldBilling/src/main найдите файл AndroidManifest.xml.
  2. Удалите тег <application>. В тег <manifest> добавьте атрибут package="ru.rustore.defold.billing"> (см. ниже).
    RuStoreDefoldBilling/src/main/AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">

    </manifest>

На этом настройка модуля окончена, и можно переходить к проверке работы сборки.

Проверка работы сборки

Чтобы проверить, как работает сборка, выполните следующие действия.

  1. В панели инструментов Android Studio выберите View > Tool Windows > Gradle (см. изображение ниже).


    Отображение инструмента Gradle

    В правой части окна Android Studio отобразится область Gradle.


    Инструмент Gradle

  2. Нажмите в панели инструментов области Gradle.
    Отобразится окно Run Anything.


    Окно Run Anything

  3. В верхней строке выполните команду gralde assemble.

Если все действия были выполнены верно в Defold-проекте будут созданы файлы:

  • billing_example/extension_rustore_core/lib/android/RuStoreDefoldCore.jar;
  • billing_example/extension_rustore_billing/lib/android/RuStoreDefoldBilling.jar.
подсказка

К коду, расположенному в .jar-файлах, можно обращаться точно так же, как в примере Example.java.

На этом настройка Android проекта закончена и можно переходить к реализации обёртки.

Классы для работы с JNI

Для упрощения использования JNI для наших плагинов реализуем обёртку для обратных вызовов. Идея заключается в том, что вместо создания функций JNIEXPORT JNICALL для каждого обратного асинхронного вызова мы реализуем одну очередь сообщений с одним нативным событием onMessage для передачи новых данных. Кроме того, обработчику очереди будет делегирована задача синхронизации главного и побочных потоков, которые порождает SDK RuStore.

Реализация очереди сообщений на стороне Java

Создание структуры

  1. Откройте ранее созданный проект Android Studio.
  2. Раскройте папку RuStoreDefoldCore/src/main/java/
  3. Щёлкните правой кнопкой мыши на папке ru.rustore.defold.core и выберите New > Kotlin Class/File > Object (см. изображение ниже).


    Создание нового объекта

  4. В отобразившемся окне введите значение RuStoreCore и в списке ниже выберите Object (см. изображение ниже).


    Создание объекта RuStoreCore

  5. Нажмите клавишу ВВОД, чтобы создать объект.
    примечание

    Методы обработчика очереди сообщений перечислим в интерфейсе. Для порядка поместим интерфейс в отдельный пакет.

  6. Щёлкните правой кнопкой мыши на папке ru.rustore.defold.core и выберите New > Package (см. изображение ниже).


    Создание пакета

  7. В отобразившемся окне введите значение ru.rustore.defold.core.callbacks (см. изображение ниже).


    Создание пакета

  8. Нажмите клавишу ВВОД, чтобы создать пакет.
  9. Щёлкните правой кнопкой мыши на созданном пакете callbacks и выберите New > Kotlin Class/File (см. изображение ниже).


    Создание нового интерфейса

  10. В отобразившемся окне введите значение IRuStoreChannelListener и в списке ниже выберите Interface (см. изображение ниже).


    Создание интерфейса IRuStoreChannelListener

  11. Нажмите клавишу ВВОД, чтобы создать интерфейс.
    примечание

    Интерфейс будет реализовывать Kotlin-класс RuStoreChannelListenerWrapper.

  12. Щёлкните правой кнопкой мыши на папке ru.rustore.defold.core и выберите New > Package (см. изображение ниже).


    Создание пакета

  13. В отобразившемся окне введите значение ru.rustore.defold.core.wrappers (см. изображение ниже).


    Создание пакета

  14. Нажмите клавишу ВВОД, чтобы создать пакет.
  15. Щёлкните правой кнопкой мыши на созданном пакете wrappers и выберите New > Kotlin Class/File (см. изображение ниже).
  16. В отобразившемся окне введите значение RuStoreChannelListenerWrapper и в списке ниже выберите Class (см. изображение ниже).


    Создание класса RuStoreChannelListenerWrapper

  17. Нажмите клавишу ВВОД, чтобы создать класс.

После создания структуры содержимое RuStoreDefoldCore/src/main/java/ru.rustore.defold.core должно выглядеть следующим образом.

ru.rustore.defold.core/

├── callbacks/
│ └── IRuStoreChannelListener.kt

├── wrappers/
│ └── RuStoreChannelListenerWrapper.kt

├── RuStoreCore.kt

└── AndroidManifest.xml

Cм. также изображение ниже.


Создание класса RuStoreChannelListenerWrapper

Переходите непосредственно к реализации очереди сообщений.

Реализация

IRuStoreChannelListener

Откройте ранее созданный интерфейс IRuStoreChannelListener и добавьте туда два метода:

  • fun onMessage;
  • fun disposeCppPointer.
Kotlin | IRuStoreChannelListener.kt
package ru.rustore.defold.core.callbacks

interface IRuStoreChannelListener {
}
к сведению
  • Метод onMessage будет реализовывать отправку сообщений на сторону Defold. Параметры channel и value типа String описывают канал сообщений для разделения адресатов сообщений и полезные данные сообщения соответственно.
  • Метод disposeCppPointer используется для освобождения указателя на объект на стороне C++. Этот указатель нужен для перехода к объекту из функции в блоке extern "C".

RuStoreChannelListenerWrapper

В ранее созданном классе RuStoreChannelListenerWrapper добавьте параметр конструктора cppPointer типа Long, наследование от интерфейса IRuStoreChannelListener и реализуйте унаследованные методы (см. пример До и После ниже).

Kotlin | RuStoreChannelListenerWrapper.kt
package ru.rustore.defold.core.wrappers

class RuStoreChannelListenerWrapper {
}
к сведению
  • Конструктор класса: в конструкторе RuStoreChannelListenerWrapper мы принимаем значение указателя на объект C++ в виде параметра cppPointer: Long и сохраняем его в одноимённой переменной cppPointer в блоке init.
  • nativeOnMessage — метод объявлен как external, что означает, что его реализация находится в коде на языке C++. Именно этот метод предназначен для передачи сообщения из нативного кода на сторону скриптов Defold.
  • onMessage является реализацией метода интерфейса IRuStoreChannelListener, который вызывается при задаче отправки сообщения через JNI на сторону Defold. В методе происходит вызов nativeOnMessage с передачей указателя cppPointer, значений канала и полезной нагрузки сообщения.
  • disposeCppPointer является реализацией метода интерфейса IRuStoreChannelListener. Метод вызывается со стороны C++ при удалении объекта С++.
  • Синхронизация: для обеспечения потокобезопасности в классе используется функция synchronized. Методы onMessage и disposeCppPointer синхронизируются, чтобы избежать попыток обнуления указателя во время передачи данных.

RuStoreCore

В ранее созданном объекте RuStoreCore реализуйте методы для установки объекта обработки очереди сообщений и метод для отправки сообщения:

  • setChannelListener;
  • emitSignal.
Kotlin | RuStoreCore.kt
package ru.rustore.defold.core

object RuStoreCore {
}
к сведению
  • setChannelListener — метод используется для установки обработчика очереди сообщений с интерфейсом IRuStoreChannelListener в объект RuStoreCore.
  • emitSignal — метод используется для передачи сообщения с указанным каналом и значением обработчику очереди, установленному ранее с помощью метода setChannelListener. Во избежание ошибок в методе происходит проверка, что переменная listener инициализирована.

Переходите к реализации очереди сообщений на стороне Defold.

Реализация очереди сообщений на стороне Defold

Создание структуры

Методы для работы с очередью разместим в расширении RuStoreCore.

  1. Откройте ранее созданный проект Defold.
  2. В папке extension_rustore_core/include создайте заголовочные файлы:
    • AndroidJavaObject.h;
    • ChannelCallbackManager.h;
    • QueueCallbackManager.h;
    • RuStoreChannelListener.h.
  3. В папке extension_rustore_core/src создайте файлы исходного кода:
    • ChannelCallbackManager.cpp;
    • QueueCallbackManager.cpp;
    • RuStoreChannelListener.cpp. Структура Defold-проекта будет выглядеть следующим образом.
    extension_rustore_core/

    ├── include/
    │ ├── AndroidJavaObject.h
    │ ├── ChannelCallbackManager.h
    │ ├── QueueCallbackManager.h
    │ └── RuStoreChannelListener.h

    ├── lib/
    │ └── android/
    │ └──RuStoreDefoldCore.jar

    ├── manifests/
    │ └── android
    │ └── build.gradle

    ├── src/
    │ ├── ChannelCallbackManager.cpp
    │ ├── Example.java
    │ ├── QueueCallbackManager.cpp
    │ ├── RuStoreChannelListener.cpp
    │ └── rustorecore.cpp

    └── ext.manifest

Реализация

AndroidJavaObject

В файл extension_rustore_core/include/AndroidJavaObject.h поместите следующий код.

к сведению

Класс AndroidJavaObject служит для хранения информации о Java-объекте с которым можно взаимодействовать посредством JNI.

C++ | AndroidJavaObject.h
#pragma once

#include <dmsdk/sdk.h>
#include <dmsdk/dlib/android.h>

namespace RuStoreSDK
{
class AndroidJavaObject
{
public:
jclass cls;
jobject obj;

AndroidJavaObject()
{
cls = nullptr;
obj = nullptr;
}
};
}
к сведению
  • jclass cls - указатель на класс Java.
  • jobject obj - указатель на объект Java.

ChannelCallbackManager

  1. В файл extension_rustore_core/include/ChannelCallbackManager.h поместите класс ChannelCallbackManager и структуру ChannelCallbackItem.
    C++ | ChannelCallbackManager.h
    #pragma once

    #include <dmsdk/sdk.h>
    #include <vector>
    #include <mutex>

    namespace RuStoreSDK
    {
    struct ChannelCallbackItem
    {
    const char* channel;
    dmScript::LuaCallbackInfo* callback;

    ChannelCallbackItem(const char* ch, dmScript::LuaCallbackInfo* cb) : channel(ch), callback(cb) {}
    };

    class ChannelCallbackManager
    {
    private:
    std::mutex _mutex;
    std::vector<std::shared_ptr<ChannelCallbackItem>> _callbacks;

    public:
    void AddChannelCallback(std::shared_ptr<ChannelCallbackItem> item);
    std::vector<dmScript::LuaCallbackInfo*> FindLuaCallbacksByChannel(const char* channel);
    static ChannelCallbackManager* Instance();
    };
    }
    к сведению
    • Структура ChannelCallbackItem используется для хранения информации о канале и связанной с ним callback-функции.
    • const char* channel - указатель на строку, представляющую имя канала.
    • LuaCallbackInfo* callback - указатель на callback-функцию в Lua, которая будет вызвана при передаче сообщения в канал.
    • Класс ChannelCallbackManager реализует менеджер для управления callback-функциями, связанными с каналами. Он предоставляет методы для добавления новых callback-функций к определённому каналу и получения всех callback-функций, связанных с заданным каналом. Методы класса:
      • AddChannelCallback - метод для добавления нового канала данных;
      • FindLuaCallbacksByChannel - метод для поиска всех callback-функций, связанных с заданным каналом;
      • Instance - статический метод для получения Singleton-экземпляра класса.
  2. В файл extension_rustore_core/src/ChannelCallbackManager.cpp поместите следующий код.
    C++ | ChannelCallbackManager.cpp
    #include "ChannelCallbackManager.h"

    using namespace RuStoreSDK;

    void ChannelCallbackManager::AddChannelCallback(std::shared_ptr<ChannelCallbackItem> item)
    {
    std::lock_guard<std::mutex> lock(_mutex);

    _callbacks.push_back(item);
    }

    std::vector<dmScript::LuaCallbackInfo*> ChannelCallbackManager::FindLuaCallbacksByChannel(const char* channel)
    {
    std::lock_guard<std::mutex> lock(_mutex);

    std::vector<dmScript::LuaCallbackInfo*> result;

    for (const auto& callback : _callbacks)
    {
    if (std::strcmp(callback->channel, channel) == 0)
    {
    result.push_back(callback->callback);
    }
    }

    return result;
    }

    ChannelCallbackManager* ChannelCallbackManager::Instance()
    {
    static ChannelCallbackManager instance;

    return &instance;
    }

QueueCallbackManager

  1. В файл extension_rustore_core/include/QueueCallbackManager.h поместите класс QueueCallbackManager и структуру QueueCallbackItem.
    C++ | QueueCallbackManager.h
    #pragma once

    #include <queue>
    #include <mutex>

    namespace RuStoreSDK
    {
    struct QueueCallbackItem
    {
    const std::string channel;
    const std::string value;

    QueueCallbackItem(const std::string& channel, const std::string& value)
    : channel(channel), value(value) {
    }
    };

    class QueueCallbackManager
    {
    private:
    std::mutex _mutex;
    std::queue<std::shared_ptr<QueueCallbackItem>> _queue;

    public:
    void PushQueueCallback(std::shared_ptr<QueueCallbackItem> item);
    std::queue<std::shared_ptr<QueueCallbackItem>> GetExexuteQueueCallback(int max = -1);

    static QueueCallbackManager* Instance();
    };
    }
    к сведению
    • Структура QueueCallbackItem используется для хранения информации о канале и значении, которое должно быть передано callback-функции:
      • string channel - строка, представляющая имя канала;
      • string value - передаваемое значение.
    • Класс QueueCallbackManager реализует менеджер для управления данными для каналов сообщений. Он предоставляет методы для добавления новых данных связанных с определённым каналом и получения всех данных для передачи в каналы. Ниже представлены методы класса.
      • PushQueueCallback - метод для добавления новых данных в очередь.
      • GetExexuteQueueCallback - метод для получения очереди данных связанных с каналами. Параметр max позволяет ограничить количество данных, которые будут возвращены. При передаче в параметре max отрицательного числа будут возвращён весь доступный набор данных.
      • Instance - статический метод для получения Singleton-экземпляра класса.
  2. В файл extension_rustore_core/src/QueueCallbackManager.cpp поместите следующий код.
    C++ | QueueCallbackManager.cpp
    #include "QueueCallbackManager.h"

    using namespace RuStoreSDK;

    void QueueCallbackManager::PushQueueCallback(std::shared_ptr<QueueCallbackItem> item)
    {
    std::lock_guard<std::mutex> lock(_mutex);

    _queue.push(item);
    }

    std::queue<std::shared_ptr<QueueCallbackItem>> QueueCallbackManager::GetExexuteQueueCallback(int max)
    {
    std::lock_guard<std::mutex> lock(_mutex);

    std::queue<std::shared_ptr<QueueCallbackItem>> executeQueue;
    while (!_queue.empty() && max !=0)
    {
    auto item = _queue.front();
    executeQueue.push(item);
    _queue.pop();
    --max;
    }

    return executeQueue;
    }

    QueueCallbackManager* QueueCallbackManager::Instance()
    {
    static QueueCallbackManager instance;

    return &instance;
    }

RuStoreChannelListener

  1. В файл extension_rustore_core/include/RuStoreChannelListener.h поместите следующий код, который включает класс RuStoreChannelListener.
    C++ | RuStoreChannelListener.h
    #pragma once

    #include <dmsdk/sdk.h>
    #include <dmsdk/dlib/android.h>
    #include "QueueCallbackManager.h"

    namespace RuStoreSDK
    {
    class RuStoreChannelListener
    {
    private:
    jobject javaObject = nullptr;
    const char* signature = "Lru/rustore/defold/core/callbacks/IRuStoreChannelListener;";

    public:
    jobject GetJWrapper();
    const char* GetSignature();

    RuStoreChannelListener();
    ~RuStoreChannelListener();

    void _OnMessage(JNIEnv* env, jstring jchannel, jstring jvalue);
    static RuStoreChannelListener* Instance();
    };
    }
  2. В файл extension_rustore_core/src/RuStoreChannelListener.cpp поместите следующий код.
    C++ | RuStoreChannelListener.cpp
    #include "RuStoreChannelListener.h"

    #if defined(DM_PLATFORM_ANDROID)

    using namespace RuStoreSDK;

    void RuStoreChannelListener::_OnMessage(JNIEnv* env, jstring jchannel, jstring jvalue)
    {
    const char* channel = env->GetStringUTFChars(jchannel, 0);
    const char* value = env->GetStringUTFChars(jvalue, 0);

    auto queueCallbackItem = std::make_shared<QueueCallbackItem>((std::string(channel)), (std::string(value)));
    QueueCallbackManager::Instance()->PushQueueCallback(queueCallbackItem);

    env->ReleaseStringUTFChars(jchannel, channel);
    env->ReleaseStringUTFChars(jvalue, value);
    }

    extern "C"
    {
    JNIEXPORT void JNICALL Java_ru_rustore_defold_core_wrappers_RuStoreChannelListenerWrapper_nativeOnMessage(JNIEnv* env, jobject obj, jlong pointer, jstring channel, jstring value)
    {
    auto castobj = reinterpret_cast<RuStoreChannelListener*>(pointer);
    castobj->_OnMessage(env, channel, value);
    }
    }

    #endif
    к сведению
    • Класс реализует создание Java-объекта с интерфейсом IRuStoreChannelListener и получение обратных вызовов с данными от него в методе _OnMessage.
    • Метод _OnMessage при этом реализует наполнение данными очереди класса QueueCallbackManager.
    Рассылка данных очереди класса QueueCallbackManager по всем каналам, заданным в классе ChannelCallbackManager, реализуется в методе UpdateMyExtension в расширении RuStoreCore (extension_rustore_core/src/rustorecore.cpp). Добавление новых каналов осуществляется в методе Connect.

rustorecore.cpp

Добавьте в extension_rustore_core/src/rustorecore.cpp следующий код (см. пример До и После).

C++ | rustorecore.cpp
#define EXTENSION_NAME RuStoreCore
#define LIB_NAME "RuStoreCore"
#define MODULE_NAME "rustorecore"

#if defined(DM_PLATFORM_ANDROID)

#include <dmsdk/sdk.h>
#include <dmsdk/dlib/android.h>
#include <android/log.h>

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);
}
}
//...

#endif

static void LuaInit(lua_State* L)
//..

static dmExtension::Result InitializeMyExtension(dmExtension::Params* params)
//..

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

static dmExtension::Result FinalizeMyExtension(dmExtension::Params* params)
//..

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

Работа с очередью сообщений

После реализации очереди сообщений работа с обратными вызовами со стороны Java в Defold больше не представляет труда. На стороне Java (проект в Android Studio) достаточно подготовить данные в виде имени канала и строки данных, и вызвать метод emitSignal объекта RuStoreCore.

Ниже представлен пример кода в общем случае (файл extension_libraries/RuStoreDefoldBilling/src/main/java/ru.rustore.defold.billing/RustoreBilling.kt)

Kotlin | Передача данных по заданному каналу
import ru.rustore.defold.core.RuStoreCore

const val CHANNEL_NAME = "rustore_channel_name"

val json = %YOUR_DATA%
RuStoreCore.emitSignal(CHANNEL_NAME, json)

Здесь:

  • CHANNEL_NAME — имя канала очереди сообщений, например: CHANNEL_CHECK_PURCHASES_AVAILABLE_SUCCESS — сообщение об успешное проверки доступности платежей;
  • rustore_channel_name, например: rustore_check_purchases_available_success.
  • %YOUR_DATA% — данные ответа по заданному формату, например: """{"isAvailable": false, "cause": $cause}""" (невозможность проведения платежей с указанием причины).
подсказка

Подробнее см. пример на GitFlic.

В проекте Defold в методе connect объекта rustorecore укажите имя канала и функцию обратного вызова.

Ниже представлен пример кода в общем случае (файл billing_example/main/main.script).

Lua | Подключение callback-метода к каналу данных
function init(self)
rustorecore.connect("rustore_channel_name", _callback_method)

function _callback_method(self, channel, value)
rustorecore.show_toast(value)
end

Здесь:

  • rustore_channel_name — имя канала для очереди сообщений;
  • _callback_method — имя метода обратного вызова.
подсказка

Подробнее см. пример на GitFlic.

Методы расширений

RuStoreCore

Методы расширения RuStoreCore реализуют систему обратных вызовов Java > C++ через именованные каналы сообщений, а также работу с наиболее часто используемыми методами SDK Android. Все методы плагина, доступные для вызовов из Lua, определены в массиве Module_methods.

ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
C++ | rustorecore.cpp
static const luaL_reg Module_methods[] =
{
{"show_toast", ShowToast},
{"connect", Connect},
{"log_verbose", LogVerbose},
{"log_debug", LogDebug},
{"log_info", LogInfo},
{"log_warning", LogWarning},
{"log_error", LogError},
{"copy_to_clipboard", CopyToClipboard},
{"get_from_clipboard", GetFromClipboard},
{"get_string_resources", GetStringResources},
{"get_string_shared_preferences", GetStringSharedPreferences},
{"set_string_shared_preferences", SetStringSharedPreferences},
{"get_int_shared_preferences", GetIntSharedPreferences},
{"set_int_shared_preferences", SetIntSharedPreferences},
{0, 0}
};

show_toast

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

Ранее для примера работы с JNI мы реализовали Java-обёртку этого метода в файле Example.java. Но теперь у нас есть более подходящее место для размещения нативного кода. Создадим обертку метода в проекте Android на языке Kotlin.

Открываем в Android Studio проект extension_libraries, открываем файл RuStoreCore.kt. Файл расположен по пути RuStoreDefoldCore/src/main/java/ru.rustore.defold.core. Реализация метода на Kotlin будет следующей.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
Kotlin | Реализация метода showToast
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener

object RuStoreCore {
private var listener: IRuStoreChannelListener? = null

fun setChannelListener(listener: IRuStoreChannelListener) {
RuStoreCore.listener = listener
}

fun emitSignal(channel: String, value: String) {
listener?.run {
onMessage(channel, value)
}
}
}

Далее в проекте Defold нам необходимо исправить JNI-вызов потому что изменилось расположение Java-метода. Для этого открываем в редакторе Defold файл rustorecore.cpp и находим реализацию ShowToast.

Для обеспечения работы нового метода нам достаточно изменить путь к классу ru.rustore.defold.example.Example на новый ru.rustore.defold.core.RuStoreCore, а также заменить статические методы GetStaticMethodID и CallStaticVoidMethodID нестатическими вариантами.

Ещё одним важным дополнением станет использование метода GetJavaCoreInstance. Т.к. все методы расширения работают с Java-объектом ru.rustore.defold.core.RuStoreCore, вынесем работу по получению ссылки на объект в отдельный метод. Для этого открываем в редакторе Defold файл rustorecore.cpp и в секции #if defined(DM_PLATFORM_ANDROID) добавляем следующий код.

ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
C++ | Реализация метода GetJavaCoreInstance
#define EXTENSION_NAME RuStoreCore
//...

#if defined(DM_PLATFORM_ANDROID)
//...

using namespace RuStoreSDK;

//...
к сведению

Внутри метода выполняются следующие шаги.

  1. Получение класса ru.rustore.defold.core.RuStoreCore с помощью функции
    dmAndroid::LoadClass.
  2. Получение идентификатора статического поля INSTANCE с помощью
    env->GetStaticFieldID.
  3. Получение объекта из статического поля INSTANCE с помощью
    env->GetStaticObjectField.
  4. Сохранение класса и объекта в переданной через указатель переменной instance.

Итого получаем следующую реализацию метода ShowToast.

ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
C++ | Реализация метода ShowToast
//...
static int ShowToast(lua_State* L)
{
DM_LUA_STACK_CHECK(L, 0);

dmAndroid::ThreadAttacher thread;
JNIEnv* env = thread.GetEnv();

const char* msg = (char*)luaL_checkstring(L, 1);
jstring jmsg = env->NewStringUTF(msg);

jclass cls = dmAndroid::LoadClass(env, "ru/rustore/defold/example/Example");
jmethodID method = env->GetStaticMethodID(cls, "showToast", "(Landroid/app/Activity;Ljava/lang/String;)V");
env->CallStaticVoidMethod(cls, method, dmGraphics::GetNativeAndroidActivity(), jmsg);

env->DeleteLocalRef(jmsg);
thread.Detach();

return 0;
}
//...

connect

Метод connect предназначен для установления соединения с внешним источником данных или сервисом.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Подключение событий плагина RuStoreBilling
rustorecore.connect("rustore_check_purchases_available_success", _check_purchases_available_success)
rustorecore.connect("rustore_check_purchases_available_failure", _check_purchases_available_failure)
rustorecore.connect("rustore_on_get_products_success", _on_get_products_success)
rustorecore.connect("rustore_on_get_products_failure", _on_get_products_failure)
rustorecore.connect("rustore_on_purchase_product_success", _on_purchase_product_success)
rustorecore.connect("rustore_on_purchase_product_failure", _on_purchase_product_failure)
rustorecore.connect("rustore_on_get_purchases_success", _on_get_purchases_success)
rustorecore.connect("rustore_on_get_purchases_failure", _on_get_purchases_failure)
rustorecore.connect("rustore_on_confirm_purchase_success", _on_confirm_purchase_success)
rustorecore.connect("rustore_on_confirm_purchase_failure", _on_confirm_purchase_failure)
rustorecore.connect("rustore_on_delete_purchase_success", _on_delete_purchase_success)
rustorecore.connect("rustore_on_delete_purchase_failure", _on_delete_purchase_failure)
rustorecore.connect("rustore_on_get_purchase_info_success", _on_get_purchase_info_success)
rustorecore.connect("rustore_on_get_purchase_info_failure", _on_get_purchase_info_failure)
к сведению

Выше приведён пример подписки на все обратные вызовы (callback) плагина. Это вызовы в коде Lua. В общем случае нужно выполнить следующие действия.

  1. Реализовать метод на Java/Kotlin.
  2. Обернуть в JNI в C++.
  3. Вызвать из Lua.

Работа с rustorecore.connect описана в разделе Работа с очередью сообщений.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

log_verbose

Метод используется для записи подробных отладочных сообщений в журнал Logcat.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода log_verbose
rustorecore.log_verbose(LOG_TAG, value)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun showToast(activity: Activity, message: String) {
activity.runOnUiThread { Toast.makeText(activity, message, Toast.LENGTH_LONG).show() }
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
C++ | rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...

log_debug

Метод используется для записи отладочных сообщений в журнал Logcat.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода log_debug
rustorecore.log_debug(LOG_TAG, value)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun logVerbose(tag: String, message: String) {
Log.v(tag, message)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...

static int LogVerbose(lua_State* L)
//...

log_info

Метод используется для записи информационных сообщений в журнал Logcat.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода log_info
rustorecore.log_info(LOG_TAG, value)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun logDebug(tag: String, message: String) {
Log.d(tag, message)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int LogDebug(lua_State* L)
//...

log_warning

Метод используется для записи предупреждающих сообщений в журнал Logcat.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода log_warning
rustorecore.log_warning(LOG_TAG, value)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun logInfo(tag: String, message: String) {
Log.i(tag, message)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int LogInfo(lua_State* L)
//...

log_error

Метод используется для записи сообщений об ошибках в журнал Logcat.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода log_error
rustorecore.log_error(LOG_TAG, value)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun logWarning(tag: String, message: String) {
Log.w(tag, message)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int LogWarning(lua_State* L)
//...

copy_to_clipboard

Метод используется для копирования текстовой информации в буфер обмена устройства.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода copy_to_clipboard
rustorecore.copy_to_clipboard(value)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun logError(tag: String, message: String) {
Log.e(tag, message)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int LogError(lua_State* L)
//...

get_from_clipboard

Метод позволяет получить текстовую информацию из буфера обмена устройства.

ПроектПуть к файлу
Defoldmain/main.script
local value = rustorecore.get_from_clipboard()
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun copyToClipboard(activity: Activity, text: String) {
val clipboard: ClipboardManager = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(CLIP_DATA_TOOLTIP, text)
clipboard.setPrimaryClip(clip)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int CopyToClipboard(lua_State* L)
//...

get_string_resources

Метод позволяет получить строковые ресурсы из ресурсов приложения.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода get_string_resources
local value = rustorecore.get_string_resources(name)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun getFromClipboard(activity: Activity): String {
val clipboard: ClipboardManager = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = clipboard.primaryClip ?: return String()
val text = clip.getItemAt(0).text ?: return String()

return text.toString()
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int GetFromClipboard(lua_State* L)
//...

get_string_shared_preferences

Метод используется для получения строкового значения из настроек SharedPreferences.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода get_string_shared_preferences
local value = rustorecore.get_string_shared_preferences(STORAGE_NAME, KEY, DEFAULT)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun getStringResources(activity: Activity, name: String): String {
val application = activity.application
val id: Int = application.resources.getIdentifier(name, "string", application.packageName)

return application.getString(id)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int GetStringResources(lua_State* L)
//...

get_int_shared_preferences

Метод используется для получения целочисленного значения из настроек SharedPreferences.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода get_int_shared_preferences
local value = rustorecore.get_int_shared_preferences(STORAGE_NAME, KEY, DEFAULT)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun getStringSharedPreferences(activity: Activity, storageName: String, key: String, defaultValue: String): String {
val application = activity.application
val preferences: SharedPreferences = application.getSharedPreferences(storageName, Context.MODE_PRIVATE)

return preferences.getString(key, defaultValue).orEmpty()
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int GetStringSharedPreferences(lua_State* L)
//...

set_string_shared_preferences

Метод позволяет установить строковое значение в настройках SharedPreferences.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода set_string_shared_preferences
rustorecore.set_string_shared_preferences(STORAGE_NAME, KEY, VALUE)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun getIntSharedPreferences(activity: Activity, storageName: String, key: String, defaultValue: Int): Int {
val application = activity.application
val preferences: SharedPreferences = application.getSharedPreferences(storageName, Context.MODE_PRIVATE)

return preferences.getInt(key, defaultValue)
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int GetIntSharedPreferences(lua_State* L)
//...

set_int_shared_preferences

Метод позволяет установить целочисленное значение в настройках SharedPreferences.

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода set_ing_shared_preferences
rustorecore.set_int_shared_preferences(STORAGE_NAME, KEY, VALUE)
подсказка

Подробнее см. пример на GitFlic.

ПроектПуть к файлу
Android StudioRuStoreDefoldCore/src/main/java/ru.rustore.defold.core/RuStoreCore.kt
package ru.rustore.defold.core

import ru.rustore.defold.core.callbacks.IRuStoreChannelListener
import android.app.Activity
import android.widget.Toast

object RuStoreCore {
//...
//...

fun setStringSharedPreferences(activity: Activity, storageName: String, key: String, value: String) {
val application = activity.application
val preferences: SharedPreferences = application.getSharedPreferences(storageName, Context.MODE_PRIVATE)
val editor = preferences.edit()
editor.putString(key, value)
editor.apply()
}
}
ПроектПуть к файлу
Defoldextension_rustore_core/src/rustorecore.cpp
//...

static int ShowToast(lua_State* L)
//...
//...

static int SetStringSharedPreferences(lua_State* L)
//...

RuStoreBilling

Большинство методов расширения RuStoreBilling реализуют обёртку над соответствующим методом SDK rustorebillingclient. Все методы плагина, доступные для вызовов из Lua, определены в массиве Module_methods (см. ниже).

ПроектПуть к файлу
Defoldextension_rustore_billing/src/rustorebilling.cpp
C++ | rustorebilling.cpp
static const luaL_reg Module_methods[] =
{
{"init", Init},
{"check_purchases_availability", CheckPurchasesAvailability},
{"get_products", GetProducts},
{"purchase_product", PurchaseProduct},
{"get_purchases", GetPurchases},
{"confirm_purchase", ConfirmPurchase},
{"delete_purchase", DeletePurchase},
{"get_purchase_info", GetPurchaseInfo},
{"set_theme", SetTheme},
{"set_error_handling", SetErrorHandling},
{0, 0}
};
примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Все методы расширения работают с Java-объектом типа ru.rustore.defold.billing.RuStoreBilling. Поэтому стоит вынести работу по получению ссылки на объект в отдельный метод. Для этого открываем в редакторе Defold файл rustorebilling.cpp и в секции #if defined(DM_PLATFORM_ANDROID) добавляем следующий код.

C++ | Реализация метода GetJavaObjectInstance
static void GetJavaObjectInstance(JNIEnv* env, AndroidJavaObject* instance)
{
jclass cls = dmAndroid::LoadClass(env, "ru.rustore.defold.billing.RuStoreBilling");
jfieldID instanceField = env->GetStaticFieldID(cls, "INSTANCE", "Lru/rustore/defold/billing/RuStoreBilling;");
jobject obj = env->GetStaticObjectField(cls, instanceField);

instance->cls = cls;
instance->obj = obj;
}
к сведению

Внутри метода выполняются следующие шаги.

  1. Получение класса ru.rustore.defold.billing.RuStoreBilling с помощью функции dmAndroid::LoadClass.
  2. Получение идентификатора статического поля INSTANCE с помощью env->GetStaticFieldID.
  3. Получение объекта из статического поля INSTANCE с помощью env->GetStaticObjectField.
  4. Сохранение класса и объекта в переданной через указатель переменной instance.

init

Метод init предназначен для инициализации расширения и подготовки его к работе. Этот метод должен быть вызван прежде всех других методов расширения. Соответствует методу RuStoreBillingClientFactory.create нативного SDK.

Создадим обертку метода в проекте Android. Открываем в Android Studio ранее подготовленный проект extension_libraries и создадим файл RuStoreBilling.kt по пути RuStoreDefoldBilling/src/main/java/ru.rustore.defold.billing.

ПроектПуть к файлу
Android StudioRuStoreDefoldBilling/src/main/java/ru.rustore.defold.billing/RustoreBilling.kt
Kotlin | Реализация метода Init
private var client: RuStoreBillingClient? = null
private var isInitialized = false

fun init(activity: Activity, id: String, scheme: String, debugLogs: Boolean) {
if (isInitialized) return

client = RuStoreBillingClientFactory.create(
context = activity.application,
consoleApplicationId = id,
deeplinkScheme = scheme,
internalConfig = mapOf(
"type" to "defold"
),
themeProvider = RuStoreBillingClientThemeProviderImpl,
debugLogs = debugLogs,
externalPaymentLoggerFactory = if (debugLogs) { tag -> this.tag = tag; this } else null
)

isInitialized = true
}

Подробнее см. пример на GitFlic.

Далее в проекте Defold нам необходимо реализовать JNI-вызов созданного метода. Для этого открываем в редакторе Defold файл rustorebilling.cpp и в секции #if defined(DM_PLATFORM_ANDROID) добавляем следующий код.

ПроектПуть к файлу
Defoldextension_rustore_billing/src/rustorebilling.cpp
C++ | Реализация метода Init
static int Init(lua_State* L)
{
DM_LUA_STACK_CHECK(L, 0);

dmAndroid::ThreadAttacher thread;
JNIEnv* env = thread.GetEnv();

AndroidJavaObject instance;
GetJavaObjectInstance(env, &instance);
jmethodID method = env->GetMethodID(instance.cls, "init", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;Z)V");

const char* id = (char*)luaL_checkstring(L, 1);
const char* scheme = (char*)luaL_checkstring(L, 2);

jstring jid = env->NewStringUTF(id);
jstring jscheme = env->NewStringUTF(scheme);
jboolean jdebugLogs = false;

int n = lua_gettop(L);
if (n > 2) jdebugLogs = (jboolean)lua_toboolean(L, 3);

env->CallVoidMethod(instance.obj, method, dmGraphics::GetNativeAndroidActivity(), jid, jscheme, jdebugLogs);

env->DeleteLocalRef(jid);
env->DeleteLocalRef(jscheme);

thread.Detach();

return 0;
}

Подробнее см. пример на GitFlic.

check_purchases_availability

Метод используется для проверки доступности покупок или подписок в магазине приложений. Соответствует методу checkPurchasesAvailability нативного SDK.

Здесь мы уже будем активно использовать ранее созданную систему обратных вызовов. Открываем в Android Studio файл RuStoreBilling.kt и вставляем следующий код в объект RuStoreBilling.

ПроектПуть к файлу
Android StudioRuStoreDefoldBilling/src/main/java/ru.rustore.defold.billing/RustoreBilling.kt
Kotlin | Реализация метода checkPurchasesAvailability
const val CHANNEL_CHECK_PURCHASES_AVAILABLE_SUCCESS = "rustore_check_purchases_available_success"
const val CHANNEL_CHECK_PURCHASES_AVAILABLE_FAILURE = "rustore_check_purchases_available_failure"

fun checkPurchasesAvailability() {
client?.run {
purchases.checkPurchasesAvailability()
.addOnSuccessListener { result ->
when (result) {
is FeatureAvailabilityResult.Available -> {
RuStoreCore.emitSignal(
CHANNEL_CHECK_PURCHASES_AVAILABLE_SUCCESS,
"""{"isAvailable": true}"""
)
}
is FeatureAvailabilityResult.Unavailable -> {
val cause = JsonBuilder.toJson(result.cause)
val json = """{"isAvailable": false, "cause": $cause}"""
handleError(result.cause)
RuStoreCore.emitSignal(CHANNEL_CHECK_PURCHASES_AVAILABLE_SUCCESS, json)
}
}
}
.addOnFailureListener { throwable ->
handleError(throwable)
RuStoreCore.emitSignal(CHANNEL_CHECK_PURCHASES_AVAILABLE_FAILURE, JsonBuilder.toJson(throwable))
}
}
}

Подробнее см. пример на GitFlic.

к сведению
  • CHANNEL_CHECK_PURCHASES_AVAILABLE_SUCCESS и CHANNEL_CHECK_PURCHASES_AVAILABLE_FAILURE задают имена каналов данных.
  • Передача данных выполняется методом RuStoreCore.emitSignal.
  • Информацию о результате проверки метод checkPurchasesAvailability возвращает в событиях задаваемых через addOnSuccessListener и addOnFailureListener.
  • В случае соответствия условиям работы платежей возвращается FeatureAvailabilityResult.Available и FeatureAvailabilityResult.Unavailable в случае невыполнения условий.
  • Для упрощения обработки ответов о состоянии проверки условий работы платежей на стороне Lua вместо передачи FeatureAvailabilityResult передадим JSON-строки "{"isAvailable": true}" и "{"isAvailable": false, "cause": $cause}" вместо FeatureAvailabilityResult.Available и FeatureAvailabilityResult.Unavailable соответственно.

В проекте Defold нам также необходимо реализовать JNI-вызов метода. Открываем файл rustorebilling cpp в редакторе Defold и в секции #if defined(DM_PLATFORM_ANDROID) добавляем следующий код.

ПроектПуть к файлу
Defoldextension_rustore_billing/src/rustorebilling.cpp
C++ | Реализация метода CheckPurchasesAvailability
static int CheckPurchasesAvailability(lua_State* L)
{
DM_LUA_STACK_CHECK(L, 0);

dmAndroid::ThreadAttacher thread;
JNIEnv* env = thread.GetEnv();

AndroidJavaObject instance;
GetJavaObjectInstance(env, &instance);
jmethodID method = env->GetMethodID(instance.cls, "checkPurchasesAvailability", "()V");

env->CallVoidMethod(instance.obj, method);

thread.Detach();

return 0;
}

Подробнее см. пример на GitFlic.

к сведению

Метод выполняет следующие шаги.

  1. Через макрос DM_LUA_STACK_CHECK задаёт количество ожидаемых элементов в стеке Lua после выполнения функции. 0 – нет возвращаемых параметров.
  2. Получает JNI-интерфейс с помощью dmAndroid::ThreadAttacher и thread.GetEnv.
  3. Через метод GetJavaObjectInstance получает ссылки на Java-объект.
  4. Получает идентификатор метода checkPurchasesAvailability с помощью env->GetMethodID.
  5. Вызывает метод checkPurchasesAvailability на Java-объекте без передачи параметров env->CallVoidMethod.
  6. Освобождает JNI-ресурсы dmAndroid::ThreadAttacher.

Вызов метода в Lua должен осуществляться с передачей трёх следующих параметров.

  • CONSOLE_APPLICATION_ID — код приложения из консоли разработчика RuStore (пример: https://console.rustore.ru/apps/123456, consoleApplicationId = 123456). Подробная информация о публикации приложений в RuStore доступна на странице help.
  • DEEPLINK_SCHEME — схема deeplink, необходимая для возврата в ваше приложение после оплаты через стороннее приложение (например, SberPay или СБП).
  • DEBUG_LOGS — флаг, регулирующий ведение журнала событий. Укажите значение true, если хотите, чтобы события попадали в журнал. В ином случае укажите false.

Перед использованием метода в Lua необходимо единожды выполнить подписку на события:

  • rustore_check_purchases_available_success;
  • rustore_check_purchases_available_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_check_purchases_available_success", _check_purchases_available_success)
rustorecore.connect("rustore_check_purchases_available_failure", _check_purchases_available_failure)
end

function _check_purchases_availability_success(self, channel, value)
local data = json.decode(value)
end

function _check_purchases_availability_failure(self, channel, value)
local data = json.decode(value)
end
Lua | Вызов метода check_purchases_availability
rustorebilling.check_purchases_availability()
подсказка

Подробнее см. пример на GitFlic.

get_products

Метод предназначен для получения списка продуктов и подписок доступных для покупки в вашем приложении. Соответствует методу getProducts нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Перед использованием метода в Lua необходимо единожды выполнить подписку на события:

  • rustore_on_get_products_success;
  • rustore_on_get_products_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_on_get_products_success", _on_get_products_success)
rustorecore.connect("rustore_on_get_products_failure", _on_get_products_failure)
end

function _on_get_products_success(self, channel, value)
local data = json.decode(value)
end

func _on_get_products_failure(self, channel, value)
local data = json.decode(value)
end
Lua | Вызов метода get_products
local PRODUCT_IDS = {
"non_con2",
"non_con1",
"con2",
"con1",
"sub2",
"sub1"}

rustorebilling.get_products(PRODUCT_IDS)
подсказка

Подробнее см. пример на GitFlic.

purchase_product

Метод позволяет осуществить покупку конкретного продукта или подписки. Соответствует методу purchaseProduct нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Перед использованием метода в Lua необходимо единожды выполнить подписку на события:

  • rustore_on_purchase_product_success;
  • rustore_on_purchase_product_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_on_purchase_product_success", _on_purchase_product_success)
rustorecore.connect("rustore_on_purchase_product_failure", _on_purchase_product_failure)
end

function _on_purchase_product_success(self, channel, value)
local data = json.decode(value)
end

function _on_purchase_product_failure(self, channel, value)
local data = json.decode(value)
end
Lua | Вызов метода purchase_product
local PRODUCT_ID = "example_id"
local PARAMS = "{" ..
"\"orderId\":\"example\"," ..
"\"quantity\":1," ..
"\"payload\":\"example\"" ..
"}"

rustorebilling.purchase_product(PRODUCT_ID, PARAMS)
подсказка

Подробнее см. пример на GitFlic.

get_purchases

Метод используется для получения списка совершенных покупок или подписок. Соответствует методу getPurchases нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Перед использованием метода необходимо единожды выполнить подписку на события:

  • rustore_on_get_purchases_success;
  • rustore_on_get_purchases_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_on_get_purchases_success", _on_get_purchases_success)
rustorecore.connect("rustore_on_get_purchases_failure", _on_get_purchases_failure)
end

function _on_get_purchases_success(self, channel, value)
local data = json.decode(value)

for key, value in pairs(data) do
-- value.amount
end
end

function _on_get_purchases_failure(self, channel, value)
local data = json.decode(value)
end
Lua | Вызов метода get_purchases
rustorebilling.get_purchases()
подсказка

Подробнее см. пример на GitFlic.

confirm_purchase

Метод служит для подтверждения совершенной покупки. Соответствует методу confirmPurchase нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Перед использованием метода в Lua необходимо единожды выполнить подписку на события:

  • rustore_on_confirm_purchase_success;
  • rustore_on_confirm_purchase_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_on_confirm_purchase_success", _on_confirm_purchase_success)
rustorecore.connect("rustore_on_confirm_purchase_failure", _on_confirm_purchase_failure)
end

function _on_confirm_purchase_success(self, channel, value)
local data = json.decode(value)
end

function _on_confirm_purchase_failure(self, channel, value)
local data = json.decode(value)
end
подсказка

Подробнее см. пример на GitFlic.

Lua | Вызов метода confirm_purchase
-- Ваша реализация UI подтверждения покупки
function _on_confirm_purchase_pressed(purchaseId):
rustorebilling.confirm_purchase(purchaseId)
end

delete_purchase

Метод позволяет удалить информацию о конкретной покупке. Соответствует методу deletePurchase нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Перед использованием метода в Lua необходимо единожды выполнить подписку на события:

  • rustore_on_delete_purchase_success;
  • rustore_on_delete_purchase_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_on_delete_purchase_success", _on_delete_purchase_success)
rustorecore.connect("rustore_on_delete_purchase_failure", _on_delete_purchase_failure)
end

function _on_delete_purchase_success(self, channel, value)
local data = json.decode(value)
end

function _on_delete_purchase_failure(self, channel, value)
local data = json.decode(value)
end
Lua | Вызов метода delete_purchase
-- Ваша реализация UI отмены покупки
function _on_delete_purchase_pressed(purchaseId):
rustorebilling.delete_purchase(purchaseId)
end
подсказка

Подробнее см. пример на GitFlic.

get_purchase_info

Метод предназначен для получения дополнительной информации о конкретной покупке. Соответствует методу getPurchaseInfo нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

Перед использованием метода в Lua необходимо единожды выполнить подписку на события:

  • rustore_on_get_purchase_info_success;
  • rustore_on_get_purchase_info_failure.
ПроектПуть к файлу
Defoldmain/main.script
Lua | Подписка на события
function init(self)
-- Инициализация rustorebilling

rustorecore.connect("rustore_on_get_purchase_info_success", _on_get_purchase_info_success)
rustorecore.connect("rustore_on_get_purchase_info_failure", _on_get_purchase_info_failure)
end

function _on_get_purchase_info_success(self, channel, value)
local data = json.decode(value)
end

function _on_get_purchase_info_failure(self, channel, value)
local data = json.decode(value)
end
Lua | Вызов метода get_purchase_info
-- Ваша реализация UI запроса информации о покупке
function _on_get_purchase_info_pressed(purchaseId):
rustorebilling.get_purchase_info(purchaseId)
end
подсказка

Подробнее см. пример на GitFlic.

set_theme

Метод используется для установки темы (стиля) интерфейса расширения. Является адаптацией механизма динамической смены темы через интерфейс провайдера BillingClientThemeProvider нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода set_theme
rustorebilling.set_theme(0)
подсказка

Подробнее см. пример на GitFlic.

set_error_handling

Метод позволяет настроить обработку ошибок в расширении. Является адаптацией использования метода resolveForBilling у объектов RuStoreException нативного SDK.

примечание

Примеры реализации всех методов доступны в официальном репозитории проекта на GitFlic:

ПроектПуть к файлу
Defoldmain/main.script
Lua | Вызов метода set_error_handling
function init(self)
-- Инициализация rustorebilling

rustorebilling.set_error_handling(true)

end
подсказка

Подробнее см. пример на GitFlic.

GUI приложения

Далее рассмотрим создание простого пользовательского интерфейса (GUI), в котором будет реализовано использование всех методов SDK RuStore Billing:

  • верхнее горизонтальное меню;
  • слайдер с информацией о продуктах/покупках;
  • меню операций с продуктами;
  • меню операций с покупками.

Подготовка

  1. В папке main проекта Defold создайте субдиректорию gui.
    к сведению

    В ней будут размещены все файлы *.gui, *.gui_script, *.lua, шрифты и спрайты пользовательского интерфейса.

  2. В папке main/gui/ создайте файлы .gui для каждой из четырёх групп компонентов интерфейса (см. таблицу ниже)
    ФайлГруппа компонентов интерфейса
    main/gui/top_menu.guiВерхнее горизонтальное меню.
    main/gui/slider.guiСлайдер с информацией о продуктах/покупках.
    main/gui/product_actions.guiМеню операций с продуктами
    main/gui/purchase_actions.guiМеню операций с покупками.
  3. Поместите исходный файл шрифта в формате .ttf в папку main/gui/fonts.
    к сведению

    В настоящем руководстве для примера используется бесплатный шрифт Cygre.ttf.

Теперь нужно создать шрифт в формате .font, с которым работает Defold.

  1. В панели управления Deflod выберите File > New.
    Отобразится следующее окно.


    Окно New

  2. Выберит пункт Font и нажмите OK.
    Отобразится следующее окно.


    Окно New Font

  3. Выполните настройки, руководствуясь таблицей ниже.

    НастройкаОписание
    NameУкажите имя шрифта. В настоящей инструкции для примера будет использоваться значение text48.
    LocationУкажите папку, где будет располагаться созданный файла, в настоящем примере: main/gui/fonts.
    PreviewПоле заполнится автоматически, в нашем примере должно быть значение main/gui/fonts/text48.font.

    Окно New Font будет выглядеть следующим образом.


    Окно New Font

  4. Нажмите Create Font.
    Созданный шрифт отобразится в меню проекта Defold (см. изображение ниже).


    Новый шрифт создан

  5. Двойным щелчком мыши в интерфейсе Defold выберите созданный файл text48.font и в правой части окна в секции Properties задайте размер шрифта, установив настройку Size в значение 48 см. изображение ниже).


    Изменение размера шрифта

  6. В проекте Defold создайте папку main/gui/sprites и поместите туда файл button_orange_190_80.png — это изображение в формате PNG 190 на 80 пикселей, которая будет использоваться в качестве фона кнопки.

к сведению

Чтобы Defold мог использовать текстуру кнопки, необходимо создать текстурный атлас и разместить на нем все необходимые текстуры.

  1. Щёлкните правой кнопкой мыши на созданной папке sprites и выберите New... > Atlas (см. изображение ниже).


    Создание атласа

  2. В отобразившемся окне задайте имя атласа (в настоящем руководстве используется имя sprites) — окно будет выглядеть следующим образом.


    Окно New Atlas

  3. Нажмите Create Atlas.

  4. В навигационном меню проекта Defold выберите созданный sprites.atlas (см. изображение ниже).


    Окно New Atlas

  5. В правой части интерфейса в секции щёлкните правой кнопкой мыши и выберите Add Images... (см. изображение ниже).


    Добавление изображения

    Отобразится следующее окно.


    Выбор изображения

  6. Выберите добавленный ранее файл button_orange_190_80.png и нажмите OK.

    Defold автоматически создаст холст нужного размера и разместит на нем все добавленные изображения (см. изображение ниже).


    Атлас текстуры кнопки

    Вся необходимая подготовка к созданию UI выполнена, можем переходить к созданию «менюшек» приложения.

Создание компонентов интерфейса

Верхнее горизонтальное меню

примечание

В настоящем руководстве создание компонентов интерфейса подробно рассматривается на примере создания верхнего горизонтального меню. Ознакомьтесь с примером и создайте остальные компоненты по аналогии.

Отображение

Верхнее меню будет включать следующие кнопки.

КнопкаОписание
Проверка доступности платежейВызов метода check_purchases_availability.
Запрос списка продуктовВызов метода get_products.
Запрос списка покупокВызов метода get_purchases.

Каждому из наших .gui нужно указать, какие шрифты, спрайты и скрипты он может использовать. Для этого выполните следующие действия.

  1. Откройте для редактирования ранее созданный файл top_menu.gui.

  2. В древе панели Outline (справа) в папку Fonts добавьте шрифт text48.font (см. изображения ниже).


    Add > Fonts


    Окно Select Fonts

  3. В папку Textures добавьте атлас с кнопкой sprites.atlas (см. изображения ниже).


    Add > Textures


    Окно Select Textures

    Панель Outline будет иметь следующий вид.


    Панель Outline

    к сведению

    Теперь мы можем создавать объекты интерфейса, который в Defold называются нодами, и использовать добавленные ресурсы шрифтов и текстур для их декорирования.

    Defold имеет несколько типов нод. Сейчас нас интересуют только два типа (см. таблицу ниже).

    Тип нодыОписание
    BoxПрямоугольная нода с одним цветом, текстурой или мультикадровой анимацией.
    TextНода отображающая текст.
  4. В папке Nodes создайте три ноды типа Box (см. изображение ниже).


    Добавление ноды Box

  5. Созданным нодам в панели Properties присвойте Id:

    • availability – кнопка проверки доступности платежей;
    • products – кнопка получения списка продуктов;
    • purchases – кнопка получения списка покупок.
    к сведению

    Id – идентификатор объекта, Id должен быть уникальным для каждого объекта внутри заданного .gui.

    См. изображение ниже.


    Id объекта

  6. В свойствах каждого Box (панель Properties) в поле Texture укажите текстуру button_orange_190_80. Текстуры будут автоматически подтянутся из всех атласов, добавленных в папку Textures.


    Выбор текстуры объекта

  7. Назовите каждую кнопку согласно её предназначению. Для этого внутри каждого объекта Box создайте объекты Text и заполните следующие поля.

    ПолеОписание
    IdИдентификатор объекта, должен быть уникальным для всех объектов внутри одного Gui.
    TextОтображаемый текст:
    • Availability для ноды availability;
    • Products для ноды products;
    • Purchases для ноды purchases.
    FontИспользуемый шрифт, в настоящем примере: text48.

    См. изображения ниже.


    Выбор текстуры объекта


    Выбор текстуры объекта

    к сведению

    Каждая нода имеет богатый набор свойств для настройки внешнего вида, подробнее о настройке каждого параметра можно найти в документации Defold.

    После создания верхнего меню должен получиться следующий результат.


    Верхнее горизонтальное меню

Обработчик нажатий

Для обработчиков GUI используются скрипты с расширением .gui_script.

  1. В папке main/gui создадим файл top_menu.gui_script (см. изображения ниже).


    Создание нового скрипта


    Окно New Gui Script

  2. В навигационной панели проекта Defold выберите main > gui > top_menu.gui (см. изображение ниже).


    Окно New Gui Script

  3. В панели Outline справа ноду верхнего уровня Gui.

  4. В панели Properties ниже в поле Script укажите файл скрипта top_menu.gui_script (см. изображение ниже).


    Выбор скрипта обработки для верхнего меню

  5. Сохраните сделанные изменения и переходите к редактированию файла скрипта обработчика платежей main > gui > top_menu.gui.script.

    Defold уже создал структуру файла .gui_script, со стандартным набором функций:

    • init;
    • final;
    • update;
    • on_message;
    • on_input;
    • on_reload.

    Из них нам понадобятся только следующие:

    • init;
    • on_message;
    • on_input.
  6. Оставьте только необходимые функции и удалите комментарии, чтобы не загромождать код (см. пример До и После ниже.)

    Lua | top_menu.gui_script
    function init(self)
    -- Add initialization code here
    -- Learn more: https://defold.com/manuals/script/
    -- Remove this function if not needed
    end
    function final(self)
    -- Add finalization code here
    -- Learn more: https://defold.com/manuals/script/
    -- Remove this function if not needed
    end

    function update(self, dt)
    -- Add update code here
    -- Learn more: https://defold.com/manuals/script/
    -- Remove this function if not needed
    end
    function on_message(self, message_id, message, sender)
    -- Add message-handling code here
    -- Learn more: https://defold.com/manuals/message-passing/
    -- Remove this function if not needed
    end

    function on_input(self, action_id, action)
    -- Add input-handling code here. The game object this script is attached to
    -- must have acquired input focus:
    --
    -- msg.post(".", "acquire_input_focus")
    --
    -- All mapped input bindings will be received. Mouse and touch input will
    -- be received regardless of where on the screen it happened.
    -- Learn more: https://defold.com/manuals/input/
    -- Remove this function if not needed
    end
    function on_reload(self)
    -- Add reload-handling code here
    -- Learn more: https://defold.com/manuals/hot-reload/
    -- Remove this function if not needed
    end
  7. В методе init подключите возможность получения фокуса и прослушивания пользовательского ввода для .gui использующей данный скрипт.

    Lua | top_menu.gui_script
    function init(self)

    end
  8. В методе on_input реализуйте проверку нажатия кнопки. Для того чтобы меню могло обращаться к логике других скриптов, вы можете передать сообщение через систему msg.post.

    Lua | top_menu.gui_script
    function on_input(self, action_id, action)

    end
  9. Чтобы выполнить обработку нажатия кнопки в скрипте main.script, воспользуйтесь событием on_message.

    Lua | top_menu.gui_script
    function on_message(self, message_id, message, sender)

    end
  10. Для удобства использования и сопровождения кода вынесите Id нод и сообщений в файл top_menu_consts.lua — см. изображения и пример кода ниже.


    Создание нового Lua-модуля


    Окно New Lua Module

    Lua | top_menu_consts.lua
    TOP_MENU_RECEIVER = "top_menu#top_menu"
    TOP_MENU_AVAILABILITY = "availability"
    TOP_MENU_PRODUCTS = "products"
    TOP_MENU_PURCHASES = "purchases"

    TOP_MENU_SET_RECEIVER_MESSAGE_ID = "top_menu_set_receiver_message_id"
    TOP_MENU_BUTTON_PRESSED = "top_menu_button_pressed"

Объявленные константы могут быть использованы в любом файле .gui или .gui_script после добавления следующей строки.

require("main.gui.top_menu_consts")

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

С применением всех доработок top_menu.gui_script примет следующий вид (см. пример До и После ниже).

Lua | top_menu.gui_script
function init(self)
msg.post(".", "acquire_input_focus")
end

function on_message(self, message_id, message, sender)
if message_id == hash("top_menu_button_pressed") then
if message.button == "availability" then
rustorebilling.show_toast("Hello JNI")
end
end
end

function on_input(self, action_id, action)
local button_name = "availability"
local button = gui.get_node(button_name)
if gui.pick_node(button, action.x, action.y) then
msg.post("go#main", "top_menu_button_pressed", { button = button_name })
end
end

По аналогии с верхним горизонтальным меню создайте остальные элементы пользовательского интерфейса:

Слайдер

примечание

Создайте компонент пользовательского интерфейса по аналогии ориентируясь на пример создания верхнего горизонтального меню.

ФайлОписание
main/gui/slider.guiФайл компонента интерфейса.
main/gui/slider.gui_scriptФайл обработчика GUI.
main/gui/slider_consts.luaФайл констант Lua.


Слайдер

Меню операций с покупками

примечание

Создайте компонент пользовательского интерфейса по аналогии ориентируясь на пример создания верхнего горизонтального меню.

ФайлОписание
main/gui/purchase_actions.guiФайл компонента интерфейса.
main/gui/purchase_actions.gui_scriptФайл обработчика GUI.
main/gui/purchase_actions_consts.luaФайл констант Lua


Меню операций с покупками

Меню операций с продуктами

примечание

Создайте компонент пользовательского интерфейса по аналогии ориентируясь на пример создания верхнего горизонтального меню.

ФайлОписание
main/gui/product_actions.guiФайл компонента интерфейса.
main/gui/product_actions.gui_scriptФайл обработчика GUI.
main/gui/product_actions_consts.luaФайл констант Lua.


Меню операций с продуктами

Результат

Если все действия выполнены верно, проект .gui приложения (main/main.collection) примет следующий вид (см. изображение ниже).


Проект GUI приложения

Последним шагом в настройке нашего приложения станет наладка обработки deeplink. Deeplink в RuStore SDK платежей нужны для корректной работы со сторонними приложениями оплаты и позволяют возвращаться в игру после проведения оплаты.

Для отслеживания интентов deeplink создадим отдельную активити RuStoreIntentFilterActivity. В задачу этой активити будет входить перехват интентов в событиях onCreate и onNewIntent, передача их в SDK RuStore Billing и восстановление игровой активити.

В папке extension_rustore_billing/src создайте папку java и в ней файл RuStoreIntentFilterActivity.java.

Java | RuStoreIntentFilterActivity.java
package ru.rustore.defold.billing;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

public class RuStoreIntentFilterActivity extends Activity {

private final String defoldActivityClassName = "com.dynamo.android.DefoldActivity";

private Class<?> getActivityClass(String activityClassName) {
Class<?> activityClass = null;
try {
activityClass = Class.forName(activityClassName);
} catch(ClassNotFoundException ex) {
}

return activityClass;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

if (savedInstanceState == null) {
RuStoreBilling.onNewIntent(getIntent());
startGameActivity(null, defoldActivityClassName);
}
}

@Override
public void onNewIntent(Intent newIntent)
{
super.onNewIntent(newIntent);
setIntent(newIntent);

RuStoreBilling.onNewIntent(newIntent);
startGameActivity(newIntent, defoldActivityClassName);
}

private void startGameActivity(Intent intent, String gameActivityClassName) {
Class<?> gameActivityClass = getActivityClass(gameActivityClassName);
if (gameActivityClass != null) {
Intent newIntent = new Intent(this, gameActivityClass);
if (intent != null) newIntent.putExtras(intent.getExtras());
startActivity(newIntent);
}
}
}
Методы RuStoreIntentFilterActivity
  • getActivityClass: Defold не поддерживает прямой импорт DefoldActivity через import com.dynamo.android.DefoldActivity;, поэтому метод getActivityClass использует Class.forName для получения класса по имени класса в виде строки. Это ухищрение позволит впоследствии запустить игровую активити через startActivity.
  • onCreate и onNewIntent: методы жизненного цикла активити. Здесь мы перехватываем интенты в момент приема нового интента или запуска/восстановления приложения. Передаем их в SDK RuStore Billing через метод RuStoreBilling.onNewIntent. Правильный запуск игровой активити обеспечивается вызовом метода startGameActivity.
  • startGameActivity: чтобы предотвратить перезапуск активити метод выполняет «чистку» дополнительных данных содержащихся в intent. Содержимое extras при этом сохраняется.

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

За основу возьмём манифест по умолчанию.

  1. В проекте Defold на панели Assets перейдите по пути billing_example/builtins/manifests/android
  2. Найдите файл AndroidManifest.xml скопируйте его в буфер обмена — см. изображение ниже.


    Копирование AndroidManifest.xml

  3. Вставьте скопированный файл AndroidManifest.xml по пути extension_rustore_billing/manifests/android и переименуйте его в ExtendedAndroidManifest.xml.
    Теперь нужно сделать так, чтобы Defold при сборке использовал этот манифест по умолчанию.
  4. Откройте файл файл game.project как форму — см. изображение ниже.


    Открыть game.project как форму

  5. В отобразившемся окне перейдите в раздел Android.
  6. Напротив поля Manifest нажмите на кнопке .
    Отобразится следующее окно.


    Выбор пути к новому манифесту

  7. Выберите путь к ExtendedAndroidManifest.xml и нажмите OK.
  8. Сохраните изменения game.project.
  9. В файле ExtendedAndroidManifest.xml найдите активити com.dynamo.android.DefoldActivity и измените значение параметра android:launchMode singleTask на singleInstance (см. примеры До и После ниже).
    ExtendedAndroidManifest.xml
    //..
    <activity android:name="com.dynamo.android.DefoldActivity"
    android:label="{{project.title}}"
    android:configChanges="fontScale|keyboard|keyboardHidden|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
    android:screenOrientation="{{orientation-support}}"
    android:exported="true"
    android:launchMode="singleTask">
    //..
    к сведению

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

  10. Добавьте информацию об активити ru.rustore.defold.billing.RuStoreIntentFilterActivity — Для этого в любом месте между тегами <application> и </application> вставьте следующий код.
    XML | Активити ru.rustore.defold.billing.RuStoreIntentFilterActivity
    <activity android:name="ru.rustore.defold.billing.RuStoreIntentFilterActivity"
    android:exported="true"
    android:launchMode="singleTask">
    <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <!-- Set your appscheme -->
    <data android:scheme="example" />
    </intent-filter>
    </activity>
    примечание

    Обратите внимание, что схема, указанная в data android:scheme, должна совпадать со схемой, которую вы указываете в методе init плагина RuStoreBilling.

Для проверки работы deeplink можно воспользоваться любым браузером. В любом месте на своем компьютере создайте файл index.html со следующей разметкой.

HTML | index.html
<html>
<head>
<meta charset="utf-8">
</head>
<body >
<h1>
My Deep Link Test page
<p><a href="example://www.ru">Launch</a></p>
</h1>
</body>
</html>

В ссылке example://www.ru схема (example) должна совпадать со схемой указанной в манифесте приложения и методе init. При этом хост (www.ru) в нашем случае может быть любым.

Перетащите файл index.html на окно запущенного эмулятора Android. Найдите файл в файловом менеджере и запустите в мобильном браузере (см. изображение ниже). Тап по ссылке Launch будет запускать приложение, если оно не запущено или восстанавливать окно в прежнем состоянии, если игра была запущена ранее.


Страница index.html

Сценарии использования

Проверка доступности работы с платежами

Начальный экран приложения не содержит загруженных данных и уведомлений. Тап по кнопке Availability запустит метод check_purchases_availability, для выполнения проверки доступности платежей.

Результат проверки будет показан в тосте.


Проверка доступности работы с платежами

Получение списка продуктов

Тап по кнопке Products запустит метод get_products. Будет выполнено получение и отображение списка продуктов.


Получение списка продуктов

Покупка продукта

Тап по кнопке Buy вызовет метод purchase_product. Будет выполнен запуск сценария покупки продукта с отображением шторки выбора метода оплаты.


Покупка продукта