
Давно не было статей про плагины под Godot. В движке уже даже успели поменять подход к написанию плагинов, поэтому будем разбираться походу дела. По сути, плагин Godot для Android v2 — это библиотека для Android, которая зависит от библиотеки Godot для Android и пользовательского манифеста библиотеки Android.
В этой статье расскажу про самый главный плагин для игр - Яндекс реклама. Разберемся как интегрировать рекламу в свою игру. Правда, пока только под андроид. Но уже пишу под iOS.
Интегрировать будем SDK от Yandex - самая популярная рекламная SDK для российского рынка.
Плюсом реализации будет возможность подключать несколько слотов для разных форматов. Во всех существующих SDK можно подключать только по одному слоту на формат, а в моей реализации - сколько угодно слотов. Кроме того, в той реализации будет использоваться библиотека со встроенной медиацией, что позволит вам еще лучше монетизировать свое приложение.
Плагин v2
Будем делать плагин для godot 4.3 а там ощутимо переделали подход к написанию плагинов. Подробности про написание плагинов можно почитать в официальной документации. Тут я разберу только важные моменты
Плагины Android версии 1 требовали специального gdap файла конфигурации, который использовался редактором Godot для их обнаружения и загрузки. Однако у этого подхода было несколько недостатков, главным из которых было отсутствие гибкости и отход от существующего формата, процесса доставки и установки плагинов Godot EditorExport.
Эта проблема была решена для плагинов Android версии 2 путём отказа от механизма gdap упаковки и настройки в пользу существующего формата упаковки Godot EditorExportPlugin. API EditorExportPlugin в свою очередь был расширен для корректной поддержки плагинов Android.
Шаблон плагина берем с гитхаба - это самый простой подход начать писать качественный плагин.
Структура шаблонного плагина, которую будем менять
.
+--plugin
| +--demo
| | +--addons
| | \...
| +--export_scripts_template
| | +--export_plugin.gd
| | \--plugin.cfg
| \--src
| | \--main
| | +--java
| | | \--org
| | | \--godotengine
| | | \--plugin
| | | \--android
| | | \--template
| | | \--GodotAndroidPlugin.kt
| | \--AndroidManifest.xml
| \--build.gradle.kts
+--build.gradle.kts
\--settings.gradle.kts
Начнем с переименовывания в шаблоне всех нужных названий. Начнем с файла plugin/build.gradle.kts, нужно указать правильное название плагина и имя пакета, в котором плагин будет лежать
val pluginName = "GodotYandexAds"
val pluginPackageName = "ru.kovardin.godotyandexads"
Обратите внимание, что в этом файле указывается зависимость org.godotengine:godot
, которая нужна для работы плагина.
dependencies {
implementation("org.godotengine:godot:4.3.0.stable")
}
Сразу можно указать еще одну зависимость, которая нам понадобится для разработки:
dependencies {
implementation("org.godotengine:godot:4.3.0.stable")
// yandex ads
implementation("com.yandex.android:mobileads-mediation:7.8.0.0")
}
Далее, название плагина GodotYandexAds
вписываем в AndroidManifest.xml
<meta-data
android:name="org.godotengine.plugin.v2.${godotPluginName}"
android:value="${godotPluginPackageName}.GodotYandexAds"/>
Теперь меняем название проекта, указываем в файле settings.gradle.kts новое название GodotYandexAds
. В моем случае название проекта совпадает с названием плагина
rootProject.name = "GodotYandexAds"
include(":plugin")
Теперь переименовываем файл GodotAndroidPlugin.kt в GodotYandexAds.kt и в файле меняем название класса на GodotYandexAds
. Сам файл нужно перенести в пакет ru.kovardin.godotyandexads
, и для этого нужно поменять структуру директорий, перенести его в src/ru/kovardin/godotyandexads. В файле GodotYandexAds.kt я буду описывать всю основную логику плагина
В папке demo лежит тестовый проект который удобно использовать для отладки проекта. С ним пока ничего не делаем.
Папку export_scripts_template переименовываем в export. Файл export_plugin.gd переименовываем в export_yandex_ads.gd. В файле plugin.cfg нужно указать новое название скрипта в поле script="export_yandex_ads.gd"
, заодно указываем всю остальную информацию плагина
Теперь нужно отредактировать скрипт экспорта, который по сути является расширением класса EditorPlugin
. Этот скрипт необходим для корректной работы плагина в движке. Нужно определить три метода:
_get_android_dependencies(platform, debug)
- в этом методе определяем зависимости для работы нашего плагина;_get_android_dependencies_maven_repos(platform, debug)
- тут указываем репозитории, которые будут нужны для правильной работы Яндекс рекламы;_get_android_manifest_application_element_contents(platform, debug)
- этот метод позволяет дополнить AndroidManifest.xml и дописать туда необходимую информацию.
Итоговый файл export_yandex_ads.gd будет выглядеть так:
@tool
extends EditorPlugin
# A class member to hold the editor export plugin during its lifecycle.
var export_plugin : AndroidExportPlugin
func _enter_tree():
# Initialization of the plugin goes here.
export_plugin = AndroidExportPlugin.new()
add_export_plugin(export_plugin)
func _exit_tree():
# Clean-up of the plugin goes here.
remove_export_plugin(export_plugin)
export_plugin = null
class AndroidExportPlugin extends EditorExportPlugin:
# TODO: Update to your plugin's name.
var _plugin_name = "GodotYandexAds"
func _supports_platform(platform):
if platform is EditorExportPlatformAndroid:
return true
return false
func _get_android_libraries(platform, debug):
if debug:
return PackedStringArray([_plugin_name + "/bin/debug/" + _plugin_name + "-debug.aar"])
else:
return PackedStringArray([_plugin_name + "/bin/release/" + _plugin_name + "-release.aar"])
func _get_android_dependencies(platform, debug):
return PackedStringArray([
"com.yandex.android:mobileads-mediation:7.8.0.0"
])
func _get_android_dependencies_maven_repos(platform, debug):
return PackedStringArray([
"https://android-sdk.is.com/",
"https://artifact.bytedance.com/repository/pangle",
"https://sdk.tapjoy.com/",
"https://dl-maven-android.mintegral.com/repository/mbridge_android_sdk_oversea",
"https://cboost.jfrog.io/artifactory/chartboost-ads/",
"https://dl.appnext.com/"
])
func _get_android_manifest_application_element_contents(platform, debug) -> String:
return '<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3940256099942544~3347511713"/>'
func _get_name():
return _plugin_name
В скрипте нужно правильно указать название плагина var _plugin_name = "GodotYandexAds"
Итоговая структура плагина будет выглядеть так:
.
+--plugin
| +--demo
| | +--addons
| | \...
| +--export
| | +--export_yandex_ads.gd
| | \--plugin.cfg
| \--src
| | \--main
| | +--java
| | | \--ru
| | | \--kovardin
| | | \--godotyandexads
| | | \--GodotYandexAds.kt
| | \--AndroidManifest.xml
| \--build.gradle.kts
+--build.gradle.kts
\--settings.gradle.kts
В файле GodotYandexAds.kt
я буду описывать всю основную логику плагина. А пока разберемся с папками demo и export.
В папке demo будет лежать пример игры на Godot, в котором будет использоваться плагин. Это отличный способ дебажить все что я пишу и сразу готовый пример для пользователей плагина
Папка export содержит скрипт EditorPlugin
который нужен для корректной работы плагина в движке
Сборка
В шаблоне уже все готово, чтобы собрать проект и положить его в нужную папку в игре. Достаточно запустить
./gradlew assemble
В файле plugin/build.gradle.kts есть несколько подготовленных команд
// BUILD TASKS DEFINITION
val copyDebugAARToDemoAddons by tasks.registering(Copy::class) {
description = "Copies the generated debug AAR binary to the plugin's addons directory"
from("build/outputs/aar")
include("$pluginName-debug.aar")
into("demo/addons/$pluginName/bin/debug")
}
val copyReleaseAARToDemoAddons by tasks.registering(Copy::class) {
description = "Copies the generated release AAR binary to the plugin's addons directory"
from("build/outputs/aar")
include("$pluginName-release.aar")
into("demo/addons/$pluginName/bin/release")
}
val cleanDemoAddons by tasks.registering(Delete::class) {
delete("demo/addons/$pluginName")
}
val copyAddonsToDemo by tasks.registering(Copy::class) {
description = "Copies the export scripts templates to the plugin's addons directory"
dependsOn(cleanDemoAddons)
finalizedBy(copyDebugAARToDemoAddons)
finalizedBy(copyReleaseAARToDemoAddons)
from("export")
into("demo/addons/$pluginName")
}
tasks.named("assemble").configure {
finalizedBy(copyAddonsToDemo)
}
Каждый раз при запуске сборки будет обновляться плагин в игре примере. Новая версия собранной библиотеки будет копироваться в demo/addons/GodotYandexAds. Чтобы включить плагин, нужно зайти в меню Project > Project Settings, выбрать вкладку Plugins и отметить как Enable плагин с названием "GodotYandexAds"
Интеграция Яндекс рекламы
Реализуем только часть форматов: баннер, интерстишел, ревардед, аппопен. В 99% случаях этого будет достаточно.
Особенностью моей реализации будет возможность использования нескольких плейсментов одного и того же формата. Для этого будут реализовываться мапы, в которых будут храниться все рекламы, а ключом будет идентификатор юнита(в терминологии Яндекса)
В коде мапа для баннеров выглядит так:
private var banners: MutableMap<String, BannerAdView> = mutableMapOf()
Менеджмент юнитов остается на стороне разработчика.
Баннер
Теперь пройдемся по всем нужным нам форматам рекламы и реализуем. Начнем с обычных баннеров.
Тут есть некоторые сложности. Баннер просто так не отрендерить, его нужно прицепить его к лайауту. Для этого переопределяем метод onMainCreate
private var layout: FrameLayout? = null
private val layoutParams: FrameLayout.LayoutParams? = null
override fun onMainCreate(activity: Activity): View? {
layout = FrameLayout(activity)
return layout
}
В методе onMainCreate
создается новый FrameLayout
и сохраняется в параметре layout
, который можно использовать для рендера баннера. Но до рендера нужно создать баннер и настроить все параметры баннера. Для создания нам понадобится доступ к актуальной активити:
private fun createBanner(id: String, params: Dictionary) {
val activity = activity ?: return
val banner = BannerAdView(activity)
// остальной код реализации
}
Дальше задаем нужные параметры. Нужно определить позицию баннера и задать ее через настройку лайаута
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
val position = params.getOrDefault(BANNER_POSITION, POSITION_BOTTOM) as Int
val safearea = params.getOrDefault(BANNER_SAFE_AREA, true) as Boolean
if (position == POSITION_TOP) {
layoutParams?.gravity = Gravity.TOP
if (safearea) banner.y = getSafeArea().top.toFloat()
} else { // default
layoutParams?.gravity = Gravity.BOTTOM
if (safearea) banner.y = (-getSafeArea().bottom).toFloat()
}
params
- это переменная типа Dictionary
в которой передаются настройки для баннера из самого движка Godot.
getSafeArea()
- украл у пакета для работы с AdMob. Этот метод нужен для правильного определения безопасных границ для размещения баннера
private fun getSafeArea(): Rect {
val safeInsetRect = Rect()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return safeInsetRect
}
val windowInsets: WindowInsets = activity?.getWindow()?.getDecorView()?.getRootWindowInsets()
?: return safeInsetRect
val displayCutout = windowInsets.displayCutout
if (displayCutout != null) {
safeInsetRect[displayCutout.safeInsetLeft, displayCutout.safeInsetTop, displayCutout.safeInsetRight] =
displayCutout.safeInsetBottom
}
return safeInsetRect
}
Нужно задать еще один параметр - указать размер баннера. Баннер может быть inline
или sticky
.
inline
тип рекламы позволяет разработчикам указывать максимально допустимые ширину и высоту объявления, при этом наиболее оптимальный размер рекламы определяется автоматически. Чтобы выбрать лучший размер объявления, встроенные адаптивные баннеры используют максимальную, а не фиксированную высоту. Как правило, такой формат используется в приложениях-лентах или там, где допустимо обеспечить основной фокус на рекламе.
sticky
— небольшое, автоматически обновляемое рекламное объявление, которое располагается внизу или вверху экрана приложения. Баннер не перекрывает основной контент приложения и часто используется в приложениях-играх.
Код в котором указываем размеры и тип баннера:
var sizeType = params.getOrDefault(BANNER_SIZE_TYPE, BANNER_STICKY_SIZE)
var width = params.getOrDefault(BANNER_WIDTH, 0) as Int
var height = params.getOrDefault(BANNER_HEIGHT, 0) as Int
when (sizeType) {
BANNER_INLINE_SIZE ->
banner.setAdSize(BannerAdSize.inlineSize(activity, width, height))
BANNER_STICKY_SIZE ->
banner.setAdSize(BannerAdSize.stickySize(activity, width))
}
Все параметры, которые можно передавать для настройки баннера можно посмотреть в companion object
companion object {
const val POSITION_TOP = 1
const val POSITION_BOTTOM = 0
const val BANNER_STICKY_SIZE = "sticky"
const val BANNER_INLINE_SIZE = "inline"
const val BANNER_POSITION = "position"
const val BANNER_SAFE_AREA = "safe_area"
const val BANNER_WIDTH = "width"
const val BANNER_HEIGHT = "height"
const val BANNER_SIZE_TYPE = "size_type"
}
Поехали навешивать обработчиков. В каждом обработчике мы вызываем emitSignal
, указываем название и передаем параметры. Чтобы emitSignal
отрабатывал как нужно, необходимо заранее определить все сигналы и определить какие параметры будут передаваться.
val signals: MutableSet<SignalInfo> = ArraySet()
signals.add(SignalInfo("ads_initialized"))
// banner
signals.add(SignalInfo("banner_loaded", String::class.java))
signals.add(SignalInfo("banner_failed_to_load", String::class.java, Integer::class.java))
signals.add(SignalInfo("banner_ad_clicked", String::class.java))
signals.add(SignalInfo("banner_left_application", String::class.java))
signals.add(SignalInfo("banner_returned_to_application", String::class.java))
signals.add(SignalInfo("banner_on_impression", String::class.java, String::class.java))
Для баннера создаем 6 сигналов и сохраняем их в сет signals
. Как я указал выше, в каждом обработчике будет емититься свой сигнал с нужными параметрами. Определяем все обработчики:
banner.setBannerAdEventListener(object : BannerAdEventListener {
override fun onAdLoaded() {
Log.w(tag, "YandexAds: onBannerAdLoaded")
emitSignal("banner_loaded", id)
}
override fun onAdFailedToLoad(error: AdRequestError) {
Log.w(tag, "YandexAds: onBannerAdFailedToLoad. Error: " + error.code)
emitSignal("banner_failed_to_load", id, error.code)
}
override fun onAdClicked() {
Log.w(tag, "YandexAds: onBannerAdClicked")
emitSignal("banner_ad_clicked", id)
}
override fun onLeftApplication() {
Log.w(tag, "YandexAds: onBannerLeftApplication")
emitSignal("banner_left_application", id)
}
override fun onReturnedToApplication() {
Log.w(tag, "YandexAds: onBannerReturnedToApplication")
emitSignal("banner_returned_to_application", id)
}
override fun onImpression(impression: ImpressionData?) {
Log.w(tag, "YandexAds: onBannerAdImpression");
emitSignal("banner_on_impression", id, impression?.rawData.orEmpty());
}
})
Осталось определить несколько параметров, сохранить баннер в мапе banners
и, наконец, цепляем баннер к лейауту и загружаем его
banner.setAdUnitId(id);
banner.setBackgroundColor(Color.TRANSPARENT);
banners[id] = banner
layout.addView(banner, layoutParams);
banner.loadAd(request());
Больше в метод createBanner
ничего добавлять не нужно. Но каким-то образом нужно загрузить и показать баннер из кода Godot игры. Для этого определяем методы с указанием модификатора @UsedByGodot
.
Баннер нужно сначала загрузить:
@UsedByGodot
fun loadBanner(id: String, params: Dictionary) {
godot.getActivity()?.runOnUiThread {
if (!banners.containsKey(id) || banners[id] == null) {
createBanner(id, params)
} else {
banners[id]?.loadAd(request())
}
}
}
Как раз тут используется метод createBanner
, который я определил выше. Если баннер уже создан, то он будет переиспользован.
После загрузки баннера, его нужно показать. Для этого реализуем метод showBanner
@UsedByGodot
fun showBanner(id: String) {
godot.getActivity()?.runOnUiThread {
if (banners.containsKey(id) && banners[id] != null) {
banners[id]?.visibility = View.VISIBLE
Log.d(tag, "showBanner: banner ok")
} else {
Log.w(tag, "showBanner: banner not found")
}
}
}
Пытаемся достать баннер из мапы banners
и делаем его видимым. Аналогичный метод hideBanner
@UsedByGodot
fun hideBanner(id: String) {
if (banners.containsKey(id) && banners[id] != null) {
banners[id]?.visibility = View.GONE
Log.d(tag, "hideBanner: banner ok")
} else {
Log.w(tag, "hideBanner: banner not found")
}
}
И нужно не забыть метод, который удалить баннер с лайаута и из мапы при необходимости
@UsedByGodot
fun removeBanner(id: String) {
godot.getActivity()?.runOnUiThread {
if (banners.containsKey(id) && banners[id] != null) {
layout.removeView(banners[id]) // удаление самого баннера
banners.remove(id)
Log.d(tag, "removeBanner: banner ok")
} else {
Log.w(tag, "removeBanner: banner not found")
}
}
}
Теперь есть все методы для показа баннера в игре. Покажу код, который используется для показа баннера:
var _plugin_name = "GodotYandexAds"
@onready var banner_button = $CanvasLayer/VBoxContainer/Banner
func _ready():
banner_button.pressed.connect(_on_banner_button_pressed)
if Engine.has_singleton(_plugin_name):
var ads = Engine.get_singleton(_plugin_name)
ads.banner_loaded.connect(_ad_loaded)
ads.banner_on_impression.connect(_on_impression)
ads.loadBanner("demo-banner-yandex", {"size_type": "sticky", "width": 300, "position":0})
func _on_banner_button_pressed():
if Engine.has_singleton(_plugin_name):
var ads = Engine.get_singleton(_plugin_name)
ads.showBanner("demo-banner-yandex")
func _ad_loaded(id: String):
print("_ad_loaded: " + id)
func _ad_shown(id: String):
print("_ad_shown: " + id)
func _on_impression(id: String, data: String):
print("_on_impression: " + id)
print(data)
В этом коде получаем доступ к плагину через вызов Engine.get_singleton(_plugin_name)
. Напомню, что плагин называется GodotYandexAds
. Для загрузки баннера вызываем метод
ads.loadBanner("demo-banner-yandex", {"size_type": "sticky", "width": 300, "position":0})
И передаем в словаре все необходимые параметры.
Метод _on_banner_button_pressed
вызывается при клике по кнопке в игре.
Интерстишиал
Интерстишел - это межстраничное объявление. Полноэкранный формат рекламы, встраиваемый в контент приложения во время естественных пауз, таких как переход между уровнями игры или окончание выполнения целевого действия.
Начинаем как и с баннером - добавляем новый параметр, в котором будет храниться мапа со всеми интерстишелами
private var interstitials: MutableMap<String, InterstitialAd> = mutableMapOf()
Вторым шагом добавляем нужные сигналы
signals.add(SignalInfo("interstitial_loaded", String::class.java))
signals.add(SignalInfo("interstitial_failed_to_load", String::class.java, Integer::class.java))
signals.add(SignalInfo("interstitial_failed_to_show", String::class.java, Integer::class.java))
signals.add(SignalInfo("interstitial_ad_shown", String::class.java))
signals.add(SignalInfo("interstitial_ad_dismissed", String::class.java))
signals.add(SignalInfo("interstitial_ad_clicked", String::class.java))
signals.add(SignalInfo("interstitial_on_impression", String::class.java, String::class.java))
В каждый сигнал передается как минимум один параметр - идентификатор юнита. По этому идентификатору в коде игры всегда сможем понять для какого юнита сработал сигнал
По аналогии с баннерами, описываем метод createInterstitial(id: String)
. Этот метод проще чем метод для работы с баннерами.
private fun createInterstitial(id: String) {
val activity = activity ?: return
val loader = InterstitialAdLoader(activity)
loader.setAdLoadListener(object : InterstitialAdLoadListener {
override fun onAdLoaded(interstitial: InterstitialAd) {
Log.w(tag, "onInterstitialAdLoaded")
emitSignal("interstitial_loaded", id)
interstitial.setAdEventListener(object : InterstitialAdEventListener {
override fun onAdShown() {
Log.w(tag, "onInterstitialAdShown")
emitSignal("interstitial_ad_shown", id)
}
override fun onAdFailedToShow(error: AdError) {
Log.w(tag, "onInterstitialAdFailedToShow: ${error.description}")
emitSignal("interstitial_failed_to_show", id, error.description)
}
override fun onAdDismissed() {
Log.w(tag, "onInterstitialAdDismissed")
emitSignal("interstitial_ad_dismissed", id)
}
override fun onAdClicked() {
Log.w(tag, "onInterstitialAdClicked")
emitSignal("interstitial_ad_clicked", id)
}
override fun onAdImpression(data: ImpressionData?) {
Log.w(tag, "onInterstitialAdImpression: ${data?.rawData.orEmpty()}")
emitSignal("interstitial_on_impression", id, data?.rawData.orEmpty())
}
})
interstitials[id] = interstitial
}
override fun onAdFailedToLoad(error: AdRequestError) {
Log.w(tag, "onAdFailedToLoad. error: " + error.code)
emitSignal("interstitial_failed_to_load", id, error.description)
}
})
loader.loadAd(AdRequestConfiguration.Builder(id).build())
}
У стишела(как и у ревардеда) есть важное отличие от логики баннеров. Для загрузки баннеров необходимо создать экземпляр InterstitialAdLoader
.
val loader = InterstitialAdLoader(activity)
Загрузка стишела выполняется вызовом метода loadAd
, в который передается AdRequestConfiguration
с указанием идентификатора юнита
loader.loadAd(AdRequestConfiguration.Builder(id).build())
При загрузке стишела есть два колбека:
public interface InterstitialAdLoadListener {
public abstract fun onAdFailedToLoad(error: com.yandex.mobile.ads.common.AdRequestError): kotlin.Unit
public abstract fun onAdLoaded(interstitialAd: com.yandex.mobile.ads.interstitial.InterstitialAd): kotlin.Unit
}
Подписываемся на эти колбеки и сохраняем стишел в мапу в случае удачной загрузки
loader.setAdLoadListener(object : InterstitialAdLoadListener {
override fun onAdLoaded(interstitial: InterstitialAd) {
Log.w(tag, "onAdLoaded")
emitSignal("interstitial_loaded", id)
interstitial.setAdEventListener(object : InterstitialAdEventListener {
// остальной код коллбеков
})
interstitials[id] = interstitial
}
override fun onAdFailedToLoad(error: AdRequestError) {
Log.w(tag, "onInterstitialAdFailedToLoad. error: " + error.code)
emitSignal("interstitial_failed_to_load", id, error.description)
}
})
В мапу interstitials
сохраняется экземпляр InterstitialAd
. Именно у этого класса есть метод show()
, который понадобится для показа интерстишела
Теперь определяем метод, который будем вызывать в коде игры на Godot для создания и загрузки стишела
@UsedByGodot
fun loadInterstitial(id: String) {
godot.getActivity()?.runOnUiThread {
createInterstitial(id)
}
}
Осталось написать метод для показа интерстишела в игре:
@UsedByGodot
fun showInterstitial(id: String) {
val activity = activity ?: return
godot.getActivity()?.runOnUiThread {
if (interstitials.containsKey(id) && interstitials[id] != null) {
interstitials[id]?.show(activity)
Log.d(tag, "showInterstitial: interstitial ok");
} else {
Log.w(tag, "showInterstitial: interstitial not found");
}
}
}
Можно использовать стишелы в скрипте самой игры. Пример того, как загрузить и показать стишел:
var _plugin_name = "GodotYandexAds"
@onready var interstitial_button = $CanvasLayer/VBoxContainer/Interstitial
func _ready():
interstitial_button.pressed.connect(_on_interstitial_button_pressed)
if Engine.has_singleton(_plugin_name):
var ads = Engine.get_singleton(_plugin_name)
ads.interstitial_loaded.connect(_ad_loaded)
ads.interstitial_ad_shown.connect(_ad_shown)
ads.interstitial_on_impression.connect(_on_impression)
ads.loadInterstitial("demo-interstitial-yandex")
func _process(delta):
pass
func _on_interstitial_button_pressed():
if Engine.has_singleton(_plugin_name):
var ads = Engine.get_singleton(_plugin_name)
ads.showInterstitial("demo-interstitial-yandex")
func _ad_loaded(id: String):
print("_ad_loaded: " + id)
func _ad_shown(id: String):
print("_ad_shown: " + id)
func _on_impression(id: String, data: String):
print("_on_impression: " + id)
print(data)
Со стишелом закончили, можно переходить к следующему формату
Ревардед
Ревардед(rewarded, реклама с вознаграждением) — популярный полноэкранный формат объявления, за просмотр которого пользователь получает поощрение.
Показ данной рекламы активируется пользователем, например для получения бонуса или дополнительной жизни в игре. По своей механике rewarded очень похож на стишел, но с одним дополнительным коллбеком onRewarded
. В этот коллбек передается информация о вознаграждении, которое получит пользователь при просмотре рекламы.
override fun onRewarded(reward: Reward) {
Log.w(tag, "onRewarded")
val data = Dictionary()
data.set("amount", reward.amount)
data.set("type", reward.type)
emitSignal("rewarded_rewarded", id, data)
}
В остальном реализация полностью повторяет повторяет реализацию стишела. Итоговый список сигналов, которые можно подключать для ревардед формата:
signals.add(SignalInfo("rewarded_loaded", String::class.java))
signals.add(SignalInfo("rewarded_failed_to_load", String::class.java, Integer::class.java))
signals.add(SignalInfo("rewarded_failed_to_show", String::class.java, Integer::class.java))
signals.add(SignalInfo("rewarded_ad_shown", String::class.java))
signals.add(SignalInfo("rewarded_ad_dismissed", String::class.java))
signals.add(SignalInfo("rewarded_rewarded", String::class.java, Dictionary::class.java))
signals.add(SignalInfo("rewarded_ad_clicked", String::class.java))
signals.add(SignalInfo("rewarded_on_impression", String::class.java, String::class.java))
В сигнале rewarded_rewarded
передаются данные о вознаграждении в параметре типа Dictionary
Аппопен
Аппопен(appopen, реклама при открытии приложения) — специальный формат рекламы для монетизации экранов загрузки своих приложений. Такие объявления могут быть закрыты в любое время и предназначены для показа, когда пользователи выводят ваше приложение на передний план (foreground), либо при запуске, либо при возврате в него из фонового режима (background)
Для appopen нам не нужно создавать мапу - такой юнит в приложении может быть только один.
Показываться реклама будет только при вызове приложения из бекграунда. Для этого нужно подвязаться на жизненный цикл приложения. В первую очередь, нужно добавить дополнительные зависимости в plugin/build.gradle.kts
dependencies {
// ...
implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}
То же самое нужно указать в файле plugin/export/export_yandex_ads.gd в методе _get_android_dependencies
. После добавления зависимости, в методе onMainCreate
привязываемся к жизненному циклу приложения:
override fun onMainCreate(activity: Activity): View {
val processLifecycleObserver = DefaultProcessLifecycleObserver(
onProcessCameForeground = ::showAppopen
)
ProcessLifecycleOwner.get().lifecycle.addObserver(processLifecycleObserver)
// остальной код
}
DefaultProcessLifecycleObserver
- класс, описанный в файле DefaultProcessLifecycleObserver.kt
package ru.kovardin.godotyandexads
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class DefaultProcessLifecycleObserver(
private val onProcessCameForeground: () -> Unit
) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
onProcessCameForeground()
}
}
В этом классе определен всего один метод onStart
. Как раз этот метод срабатывать при старте приложения и будет вызываться метод showAppopen
.
До того как показывать рекламу, нужно ее загрузить. Для этого реализуем метод fun loadAppopen(id: String)
, который будет доступен для вызова в коде самой игры
@UsedByGodot
fun loadAppopen(id: String) {
godot.getActivity()?.runOnUiThread {
createAppopen(id)
}
}
Метод createAppopen(id: String)
очень похож на аналогичный метод для стишела и ревардеда. Коллбеки один в один как у стишела
private fun createAppopen(id: String) {
val activity = activity ?: return
val loader = AppOpenAdLoader(activity)
loader.setAdLoadListener(object : AppOpenAdLoadListener {
override fun onAdLoaded(ad: AppOpenAd) {
Log.w(tag, "onAppopenAdLoaded")
emitSignal("appopen_loaded", id)
ad.setAdEventListener(object : AppOpenAdEventListener {
override fun onAdShown() {
Log.w(tag, "onAppopenAdShown")
emitSignal("appopen_ad_shown", id)
}
override fun onAdFailedToShow(error: AdError) {
Log.w(tag, "onAppopenAdFailedToShow: ${error.description}")
emitSignal("appopen_failed_to_show", id, error.description)
}
override fun onAdDismissed() {
Log.w(tag, "onAppopenAdDismissed")
emitSignal("appopen_ad_dismissed", id)
}
override fun onAdClicked() {
Log.w(tag, "onAppopenAdClicked")
emitSignal("appopen_ad_clicked", id)
}
override fun onAdImpression(data: ImpressionData?) {
Log.w(tag, "onAppopenAdImpression: ${data?.rawData.orEmpty()}")
emitSignal("appopen_on_impression", id, data?.rawData.orEmpty())
}
})
appopen = ad
}
override fun onAdFailedToLoad(error: AdRequestError) {
Log.w(tag, "onAppopenAdFailedToLoad. error: " + error.code)
emitSignal("appopen_failed_to_load", id, error.description)
}
})
loader.loadAd(AdRequestConfiguration.Builder(id).build())
}
И, как и для стишела. делаем набор сигналов для предачи информации в код игры
signals.add(SignalInfo("appopen_loaded", String::class.java))
signals.add(SignalInfo("appopen_failed_to_load", String::class.java, Integer::class.java))
signals.add(SignalInfo("appopen_failed_to_show", String::class.java, Integer::class.java))
signals.add(SignalInfo("appopen_ad_shown", String::class.java))
signals.add(SignalInfo("appopen_ad_dismissed", String::class.java))
signals.add(SignalInfo("appopen_ad_clicked", String::class.java))
signals.add(SignalInfo("appopen_on_impression", String::class.java, String::class.java))
Осталось загрузить appopen и наслаждаться бесконечными видами рекламы
extends Node2D
var _plugin_name = "GodotYandexAds"
func _ready():
if Engine.has_singleton(_plugin_name):
var ads = Engine.get_singleton(_plugin_name)
ads.appopen_loaded.connect(_on_ad_loaded)
ads.appopen_ad_shown.connect(_on_ad_shown)
ads.appopen_on_impression.connect(_on_impression)
ads.loadAppopen("demo-appopenad-yandex")
func _on_ad_loaded(id: String):
print("_ad_loaded: " + id)
func _on_ad_shown(id: String):
print("_ad_shown: " + id)
func _on_impression(id: String, data: String):
print("_on_impression: " + id)
print(data)
Теперь, когда приложение или игра будет уходить в фон, а потом возвращаться, то на экране будет показан новый баннер
Заключение
Плагин уже можно использовать в играх и проложениях, но есть простор для развития. Можно реализовать больше доступных форматов. Кроме того, Яндекс реклама предоставляет дополнительный функционал для подбора рекламы. Такое таргетирование неплохо было бы вынести на уровень кода игры
Кроме методов для показа рекламы, в плагине реализованы еще несколько методов для отладки. Например метод для включения логирования:
@UsedByGodot
fun enableLogging(value: Boolean) {
MobileAds.enableLogging(value)
}
Код плагина доступен на гитфлик
Ссылки
- Как делать плагины под андроид
- Еще один вариант интеграции Yandex Ads в Godot
- Плагин для интеграции AdMob в Godot игру
- Шаблон для создания плагинов под Godot