这份文档里的每一步、每一个坑,都是在真机上一遍遍试出来的,不是照抄文档写的。
实测机型:Redmi 23013RK75C(Android 13 / Adreno 730),Godot 4.6.stable,
插件:Poing Studios godot-admob v4.0.1。为什么值得专门写一份:这个插件在 Godot 4.6 上有 三个必须手动打补丁 的地方,
不打的话要么 403、要么 JNI 报错、要么横幅完全静默失败(最难查,本文重点)。
一、先说结论:要打三个补丁
| # | 症状 | 根因 | 补丁位置 |
|---|---|---|---|
| 1 | 广告请求返回 403,拿不到广告 | 插件写死 play-services-ads:24.9.0,该版本已被 Google 停服 |
addons/admob/android/bin/ads/poing_godot_admob_ads.gd |
| 2 | 报错 Array[String] does not have the same element type |
Godot 4.6-stable 引擎 bug:java_class_wrapper.cpp:202 把 && 写成 \|\|,任何 typed Array 传 String[] 都失败 |
src/api/core/AdRequest.gd + 4 处调用点 |
| 3 | 横幅既不成功也不失败,SDK 一个网络包都不发 | GDScript int 传给 Java 时被装箱成 Long,插件 aar 里是 checkcast Integer,抛异常后被 JNI 静默吞掉 |
src/api/AdView.gd |
补丁 1、2 相对好查(有明确报错)。补丁 3 才是真正卡人的地方,下面第五节详细讲。
二、环境清单
| 项目 | 版本 / 说明 |
|---|---|
| Godot | 4.6.stable(用 _console.exe 版本,命令行能看到完整输出) |
| AdMob 插件 | Poing Studios godot-admob v4.0.1 |
play-services-ads |
25.5.0(跟随 Maven latest,不能用插件默认的 24.9.0) |
| 渲染器 | Mobile(Vulkan) |
| Android 构建模板 | 必须安装(Gradle 构建,约 200MB) |
| adb | Android SDK platform-tools |
| 测试机 | 开启 USB 调试,屏幕保持解锁 |
三、第 1 步:建项目
3.1 路径必须全 ASCII
✅ C:\Users\xxx\Documents\AdMobPortrait
❌ C:\Users\xxx\Documents\实验广告
Godot 导出到中文路径会失败,这是硬限制。
3.2 project.godot 的关键项
config/name="AdMobPortrait"
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.6", "Mobile")
config/icon="res://icon.svg" ; 不写会报 "No project icon specified"
[autoload]
AdManager="*res://AdManager.gd"
[display]
window/size/viewport_width=720
window/size/viewport_height=1280
window/handheld/orientation=1 ; 1 = 竖屏
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[rendering]
renderer/rendering_method="mobile"
renderer/rendering_method.mobile="mobile"
textures/vram_compression/import_etc2_astc=true ; ★ 缺了导出会失败
⚠ 坑:导出报 due to configuration errors: 但消息是空的
ERROR: Cannot export project with preset "portrait" due to configuration errors:
(后面什么都没有)
根因在引擎 platform/android/export/export_plugin.cpp:3047,
has_valid_project_configuration() 里有一段不写任何错误信息就直接置 false 的判定:
if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
valid = false; // ← 没有 r_error,所以错误消息为空
}
解决:补上 textures/vram_compression/import_etc2_astc=true。
四、第 2 步:装插件
4.1 放插件并启用
把 addons/admob 整个目录放进项目,然后 project.godot 加:
[editor_plugins]
enabled=PackedStringArray("res://addons/admob/plugin.cfg")
4.2 ★ 必须生成一次全局类名缓存
新建项目第一次打开时,脚本里的 AdView / MobileAds / AdRequest 等全局类名
全部报 “not declared”,因为 .godot/global_script_class_cache.cfg 还没生成。
godot --headless --editor --quit
跑一次(会自动生成缓存然后退出),再打开编辑器/导出就正常了。
4.3 安装 Android 构建模板
编辑器菜单:项目 → 安装 Android 构建模板,会生成 android/build/(约 200MB)。
如果从别的项目拷现成的 android/ 目录,要剔除 build/build、.gradle,
但 assetPackInstallTime 是模板自带的,别删。
五、第 3 步:三个必打的补丁 ★核心章节
补丁 1 —— play-services-ads 升到 25.5.0(解决 403)
文件:addons/admob/android/bin/ads/poing_godot_admob_ads.gd
# 原值 24.9.0 已被 Google 停服,服务端直接返回 403
var _dependency_library := ["com.google.android.gms:play-services-ads:25.5.0"]
版本号别写死,去
https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-ads/maven-metadata.xml
看<latest>是多少,跟着走。
补丁 2 —— typed Array 换 PackedStringArray(解决 JNI 报错)
根因:Godot 4.6-stable 的 platform/android/java_class_wrapper.cpp:202:
if (arr.is_typed() && (builtin != STRING || builtin != STRING_NAME))
// ^^ 应该是 &&
|| 导致这个条件恒为真,于是任何 typed Array 传 String[] 都被拒。
(master / 4.7-dev 已修正为 &&。)
改法 —— 在 src/api/core/AdRequest.gd 加一个转换函数:
func jni_keywords() -> PackedStringArray:
return PackedStringArray(keywords)
然后把 4 处调用点 的第 3 个参数改掉:
| 文件 | 行 | 改法 |
|---|---|---|
src/api/AdView.gd |
89 | ... ad_request.jni_keywords()) |
src/api/InterstitialAdLoader.gd |
44 | ad_request.jni_keywords() |
src/api/RewardedAdLoader.gd |
44 | ad_request.jni_keywords() |
src/api/RewardedInterstitialAdLoader.gd |
44 | ad_request.jni_keywords() |
另外 src/api/MobileAds.gd:42 也要改:
_plugin.set_request_configuration(
request_configuration.convert_to_dictionary(),
PackedStringArray(request_configuration.test_device_ids)) # ← 加 PackedStringArray
补丁 3 —— Integer 装箱(解决横幅完全静默失败)★最难查
症状
插页、奖励都正常,只有横幅:发出加载请求之后,
既没有 on_ad_loaded,也没有 on_ad_failed_to_load,SDK 一个包都不发。
日志里干干净净,什么都不报错。
排查三步法(这套手法可以复用到别的插件问题)
第 1 步:对照插件日志找断点
adb logcat -d -v brief | grep -i "poing-godot-admob\|godot "
正常应该看到 safeInsetRect 之后紧跟着:
D/poing-godot-admob: Safe Area of screen: Rect(...).
D/poing-godot-admob: marginTop: 0
如果 Safe Area of screen: 从没出现,说明 Banner 构造中途就断了。
第 2 步:看视图层级,确认 AdView 到底建出来没有
adb shell dumpsys activity top
往下找 godot_fragment_container,正常应该能看到
com.google.android.gms.ads.AdView。看不到就说明 addView 那段没执行。
第 3 步:反编译 aar 看字节码
unzip -o addons/admob/android/bin/ads/libs/poing-godot-admob-ads-release.aar
cd <解压目录> && unzip -o classes.jar
javap -p -c com/poingstudios/godot/admob/ads/adformats/Banner.class
在 Banner.<init> 的字节码里会看到:
63: checkcast java/lang/Integer // ad_position
convertToAdSize 里也有两处(width / height)。
根因
GDScript int ──写进 Dictionary──▶ Godot 装箱成 java.lang.Long
│
Java 侧 checkcast java/lang/Integer
│
ClassCastException
│
被 JNI 层静默吞掉 → Banner 构造中断
│
runOnUiThread{ 创建 AdView + addView } 永不执行
│
没有 AdView → 零网络请求 → 零回调
Godot 的 int 是 int64,装箱成 Long;而插件 aar 里的 Kotlin 是 this["ad_position"] as Int。
这个异常发生在 safeArea 属性初始化之后、runOnUiThread 之前,
所以 Banner 没被加进 banners 列表,后续 load_ad(uid) 取 banners[uid] 越界 —— 整条链静默。
注意:GitHub master 上的
DictionaryExtensions.kt已经改成安全的
(this[key] as? Number)?.toInt(),但发布的 aar 4.0.1 仍是旧代码。
这就是为什么”看源码明明修过了,跑起来还是崩”。
修复代码
文件:addons/admob/src/api/AdView.gd
# Godot 的 JavaClass 构造器方法名 = 类的简单名
# (java_class_wrapper.cpp:1488: java_constructor_name = 类名最后一段)
# 所以是 Integer.Integer(v),走 env->NewObjectA(..., "<init>", ...),
# 返回 JavaObject;JavaObject 进 Dictionary 时引擎直接透传 jobject。
static var _integer_class = null
static var _integer_class_ready := false
static func _java_int(value: int):
if not _integer_class_ready:
_integer_class_ready = true
if OS.get_name() == "Android":
_integer_class = JavaClassWrapper.wrap("java.lang.Integer")
if _integer_class == null:
return value
return _integer_class.Integer(value)
然后 _init() 里三个 int 全部走它:
var ad_view_dictionary := {
"ad_unit_id": ad_unit_id,
"ad_position": _java_int(ad_position),
"ad_size": {
"width": _java_int(ad_size.width),
"height": _java_int(ad_size.height)
}
}
为什么插页/奖励不受影响
它们的 AdRequest.convert_to_dictionary() 里只有 String 和空 Dictionary,
没有 int,碰不到这个 checkcast。这也解释了”为什么偏偏只有横幅坏”。
六、第 4 步:业务代码
6.1 官方测试广告单元 ID(任何包名下都能出测试广告)
const BANNER_ID := "ca-app-pub-3940256099942544/6300978111"
const INTERSTITIAL_ID := "ca-app-pub-3940256099942544/1033173712"
const REWARDED_ID := "ca-app-pub-3940256099942544/5224354917"
6.2 注册测试设备
真机上跑测试广告必须把设备注册成测试设备,否则拿不到测试广告。
设备哈希从 logcat 里 SDK 自己打印的那句取:
Use RequestConfiguration.Builder().setTestDeviceIds(Arrays.asList("599DD0E66111F3FAC24560DEABA32062"))
adb logcat -d | grep -i "setTestDeviceIds"
6.3 AdManager.gd(AutoLoad)
func _ready() -> void:
if OS.get_name() != "Android" and OS.get_name() != "iOS":
return
# 1) 注册测试设备
var cfg := RequestConfiguration.new()
cfg.test_device_ids = [TEST_DEVICE_ID]
MobileAds.set_request_configuration(cfg)
# 2) 初始化(异步)
var listener := OnInitializationCompleteListener.new()
listener.on_initialization_complete = func(_status: InitializationStatus) -> void:
_initialized = true
MobileAds.initialize(listener)
横幅(贴在竖屏底部):
func load_banner() -> void:
if _banner_view != null:
_banner_view.destroy()
_banner_view = null
var ad_size := AdSize.get_current_orientation_anchored_adaptive_banner_ad_size(AdSize.FULL_WIDTH)
var view := AdView.new(BANNER_ID, ad_size, AdPosition.Values.BOTTOM)
view.ad_listener.on_ad_loaded = func() -> void:
view.show() # 加载成功才 show
view.ad_listener.on_ad_failed_to_load = func(err: LoadAdError) -> void:
print("横幅失败 code=%d %s" % [err.code, err.message])
_banner_view = view
view.load_ad(AdRequest.new())
插页:
func load_interstitial() -> void:
var cb := InterstitialAdLoadCallback.new()
cb.on_ad_loaded = func(ad: InterstitialAd) -> void:
_interstitial = ad
ad.full_screen_content_callback.on_ad_dismissed_full_screen_content = func() -> void:
_interstitial.destroy()
_interstitial = null
load_interstitial() # 关闭后自动预加载下一则
cb.on_ad_failed_to_load = func(err: LoadAdError) -> void:
print("插页失败 code=%d %s" % [err.code, err.message])
InterstitialAdLoader.new().load(INTERSTITIAL_ID, AdRequest.new(), cb)
奖励:
func show_rewarded() -> void:
if _rewarded == null:
load_rewarded()
return
var listener := OnUserEarnedRewardListener.new()
listener.on_user_earned_reward = func(item: RewardedItem) -> void:
print("发放奖励 type=%s amount=%d" % [item.type, item.amount])
_rewarded.show(listener)
6.4 界面布局要点
横幅是 Android 原生 View,叠在 Godot 画面上方,会盖住底部内容。
所以 UI 底部要留白:
root.add_theme_constant_override("margin_bottom", 140) # 给横幅留位
6.5 时序
MobileAds.initialize 是异步的,别在 _ready() 里立刻发请求:
await get_tree().create_timer(2.5).timeout
AdManager.load_interstitial()
AdManager.load_rewarded()
await get_tree().create_timer(3.0).timeout
AdManager.load_banner()
七、第 5 步:导出 APK
7.1 export_presets.cfg 最小配置
[preset.0]
name="portrait"
platform="Android"
export_path="./AdMobPortrait.apk"
[preset.0.options]
gradle_build/use_gradle_build=true
gradle_build/export_format=0
architectures/arm64-v8a=true
package/unique_name="com.example.admobportrait"
package/signed=true
version/code=1
version/name="1.0"
screen/immersive_mode=true
7.2 ★ 每次导出前先清 *.tmp
中断导出会留下
android/build/res/values-*/godot_project_name_string.xmlNNNN.tmp,
下次构建直接失败:
mergeStandardDebugResources FAILED - The file name must end with .xml
find android/build/res -name "*.tmp" -delete
7.3 导出命令
godot --headless --export-debug "portrait" "C:/path/to/AdMobPortrait.apk"
首次 Gradle 构建约 7~15 分钟(之后有缓存会快很多)。
APK 出现在 build/ 下时就已构建完成,Godot 进程可能还在做收尾。
八、第 6 步:装到手机
⚠ 坑:INSTALL_FAILED_USER_RESTRICTED: Install canceled by user
adb install -r -t app.apk
# → INSTALL_FAILED_USER_RESTRICTED: Install canceled by user
试过 keyevent 224 唤醒、wm dismiss-keyguard、pm install、文件管理器安装,都无效。
真正的原因是 AppOps 权限被关:
adb shell appops set com.android.shell REQUEST_INSTALL_PACKAGES allow
adb install -r -t app.apk
# → Success
在 Git Bash 下记得
export MSYS_NO_PATHCONV=1,否则/data/local/tmp/...
这类路径会被自动转换。
九、第 7 步:验证广告真的出来了
9.1 抓日志
adb logcat -c # 先清空
adb shell monkey -p com.example.admobportrait -c android.intent.category.LAUNCHER 1
sleep 14
adb logcat -d -v brief | grep -i "poing-godot-admob\|godot "
9.2 健康日志长这样
[07:23:02] 正在初始化 MobileAds ...
[07:23:03] MobileAds 初始化完成 ✔
[07:23:06] 插页:已发起加载请求 ...
[07:23:06] 奖励:已发起加载请求 ...
[07:23:09] 横幅尺寸:411 x 64 dp
[07:23:09] 横幅:java.lang.Integer 装箱 ✔
D/poing-godot-admob: Safe Area of screen: Rect(0, 104 - 0, 0).
D/Ads: AdRequestServiceImpl: Sending request: .../mads/gma ← 真的发出请求了
[07:23:11] 横幅:加载成功 ✔ → 自动展示
[07:23:11] 横幅:已展示(impression)
[07:23:11] 插页:加载成功 ✔ 可以展示了
[07:23:11] 奖励:加载成功 ✔ 可以展示了
9.3 自动点按钮
Godot 的按钮是自绘的,uiautomator 定位不到,只能用坐标点:
adb shell wm size # 确认分辨率,例如 1080x2400
adb shell input tap 540 690 # 点"显示插页广告"
9.4 截图确认
adb exec-out screencap -p > screen.png
底部能看到 Google 测试横幅(「测试广告 / Nice job! This is a 320×50 test ad.」)即为成功。
十、排错速查表
| 现象 | 根因 | 解决 |
|---|---|---|
导出报 due to configuration errors: 消息为空 |
import_etc2_astc 未开 |
project.godot 加 textures/vram_compression/import_etc2_astc=true |
脚本报 AdView / MobileAds not declared |
全局类名缓存未生成 | 跑一次 godot --headless --editor --quit |
No project icon specified |
缺图标 | 加 config/icon="res://icon.svg" |
mergeStandardDebugResources FAILED ... must end with .xml |
上次导出中断留了 *.tmp |
find android/build/res -name "*.tmp" -delete |
INSTALL_FAILED_USER_RESTRICTED |
AppOps 权限被关 | adb shell appops set com.android.shell REQUEST_INSTALL_PACKAGES allow |
| 广告请求 403 | play-services-ads 版本停服 |
补丁 1,升到 Maven latest |
Array[String] does not have the same element type |
Godot 4.6 typed-array bug | 补丁 2,改 PackedStringArray |
| 横幅无任何回调、SDK 不发请求 | int 装箱成 Long,checkcast Integer 崩溃 |
补丁 3,_java_int() |
| 插页/奖励一直”未准备好” | 没等初始化完成就发请求 | 初始化回调后再等 2~3 秒 |
| 横幅盖住 UI 底部 | 原生 View 叠在画面上 | UI 底部留 140px 空白 |
十一、换成自己的广告 ID 时要做什么
- AdMob 后台创建应用 + 三个广告单元,拿到真实 ID,替换 3 个常量。
- 把测试设备注册那段删掉或改成只在 debug 生效,否则线上用户拿不到真实广告。
- 改包名
package/unique_name为你自己的域名反写。 - 发布前换成 release 导出(
--export-release)并配置正式签名。 - 欧盟/英国用户需要接 UMP consent 流程(插件里
PoingGodotAdMobUserMessagingPlatform已提供)。
附:本项目文件清单
AdMobPortrait/
├── project.godot 竖屏 + AutoLoad + import_etc2_astc
├── export_presets.cfg preset 名 "portrait"
├── AdManager.gd 广告管理器(AutoLoad)
├── Main.gd / Main.tscn 竖屏演示 UI(纯代码搭建)
├── icon.svg
├── addons/admob/ 插件(含 3 处补丁)
│ ├── src/api/AdView.gd ← 补丁 3
│ ├── src/api/core/AdRequest.gd ← 补丁 2
│ ├── src/api/MobileAds.gd ← 补丁 2
│ ├── src/api/{Interstitial,Rewarded,RewardedInterstitial}AdLoader.gd ← 补丁 2
│ └── android/bin/ads/poing_godot_admob_ads.gd ← 补丁 1
├── android/ Android 构建模板
└── build/
├── admob-portrait.apk 已验证可安装的 debug 包
└── screen_*.png 真机验证截图