Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when targeting Android/iOS — export and signing, permissions, plugins, in-app purchases, ads, app lifecycle, device features, and mobile performance
.claude/skills/jame581-mobile-development/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 86% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 161% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 103% | 0% |
Ship a Godot 4.x game to Android and iOS. This covers the platform-specific deltas beyond a generic export: signing, lifecycle, permissions, plugins, IAP, device features, and the mobile renderer/perf budget.
> Related skills: export-pipeline for the generic export flow and CI/CD, responsive-ui for safe-area layout, input-handling for touch, godot-optimization for mobile performance, csharp-godot for C# mobile caveats.
Android: OpenJDK 17 and the Android SDK; set Java SDK Path + Android SDK Path in Editor Settings (per-user, not per-project). Generate a release keystore:
bashkeytool -v -genkey -keystore mygame.keystore -alias mygame -keyalg RSA -validity 10000
Preset fields: Release / Release User / Release Password (keystore and key passwords must currently match); uncheck Export With Debug. AAB is mandatory for new Play uploads. CI env overrides: GODOT_ANDROID_KEYSTORE_RELEASE_{PATH,USER,PASSWORD}.
iOS: macOS + Xcode. Export needs an App Store Team ID + a reverse-DNS bundle Identifier; Godot generates an .xcodeproj you build from Xcode. The iOS simulator supports the Compatibility renderer only.
A custom Gradle build (Project → Install the Gradle Build template) is required for v2 plugins and IAP (Godot 4.2+). Since Godot 4.7 the Use Gradle Build export option is no longer marked experimental (GH-119172) — treat it as the standard path when you need plugins or IAP.
> ⚠️ Changed in Godot 4.7: Deprecated Google Play OBB expansion-file support was removed from the Android export. Projects still relying on APK expansion files must migrate to Play Asset Delivery or PCK patching. See GH-118283.
Real Node notification constants: NOTIFICATION_APPLICATION_PAUSED (2015), NOTIFICATION_APPLICATION_RESUMED (2014), NOTIFICATION_APPLICATION_FOCUS_IN/_OUT (2016/2017), NOTIFICATION_WM_GO_BACK_REQUEST (1007, Android Back). There is no WM_CLOSE_REQUEST on mobile. Autosave on PAUSED; iOS gives ~5 s after pause to finish work before it kills the app.
gdscriptfunc _notification(what: int) -> void: match what: NOTIFICATION_APPLICATION_PAUSED: SaveManager.save_game() # App backgrounded — persist now. NOTIFICATION_WM_GO_BACK_REQUEST: _confirm_quit() # Android Back button.
The docs only show NotificationWMCloseRequest verbatim; these PascalCase names follow the same convention.
csharppublic override void _Notification(int what) { switch ((long)what) { case NotificationApplicationPaused: SaveManager.SaveGame(); // App backgrounded — persist now. break; case NotificationWMGoBackRequest: ConfirmQuit(); // Android Back button. break; } }
DisplayServer.pip_mode_enter(window_id = 0) enters picture-in-picture mode; is_in_pip_mode(window_id = 0) reports the current state; pip_mode_set_aspect_ratio(numerator, denominator, window_id = 0) sets the PiP window's aspect ratio; pip_mode_set_auto_enter_on_background(auto_enter_on_background, window_id = 0) enters PiP automatically when the app goes to the background. Transitions arrive as Node notifications: NOTIFICATION_APPLICATION_PIP_MODE_ENTERED (2019) / NOTIFICATION_APPLICATION_PIP_MODE_EXITED (2020). All Android-only.
gdscriptfunc _ready() -> void: DisplayServer.pip_mode_set_aspect_ratio(16, 9) DisplayServer.pip_mode_set_auto_enter_on_background(true) func _notification(what: int) -> void: match what: NOTIFICATION_APPLICATION_PIP_MODE_ENTERED: _set_minimal_hud(true) # PiP window is tiny — hide non-essential UI. NOTIFICATION_APPLICATION_PIP_MODE_EXITED: _set_minimal_hud(false)
csharppublic override void _Ready() { DisplayServer.PipModeSetAspectRatio(16, 9); DisplayServer.PipModeSetAutoEnterOnBackground(true); } public override void _Notification(int what) { switch ((long)what) { case NotificationApplicationPipModeEntered: SetMinimalHud(true); // PiP window is tiny — hide non-essential UI. break; case NotificationApplicationPipModeExited: SetMinimalHud(false); break; } }
Declare each permission in the export preset as permissions/<name>; request it at runtime with OS.request_permission(name). The result arrives via MainLoop's on_request_permissions_result(permission, granted). The permission must also be enabled in the preset, not just requested.
gdscriptfunc _ready(): if "android.permission.POST_NOTIFICATIONS" not in OS.get_granted_permissions(): OS.request_permission("android.permission.POST_NOTIFICATIONS") get_tree().on_request_permissions_result.connect(_on_perm_result) func _on_perm_result(permission: String, granted: bool): print("%s granted: %s" % [permission, granted])
csharppublic override void _Ready() { if (!OS.GetGrantedPermissions().Contains("android.permission.POST_NOTIFICATIONS")) OS.RequestPermission("android.permission.POST_NOTIFICATIONS"); GetTree().OnRequestPermissionsResult += OnPermResult; } private void OnPermResult(string permission, bool granted) => GD.Print($"{permission} granted: {granted}");
Godot 4.4+ only. JavaClassWrapper.wrap("<java.class>") calls Java/Kotlin classes with no plugin; the AndroidRuntime singleton (Engine.get_singleton("AndroidRuntime")) exposes getActivity(), getApplicationContext(), and createRunnableFromGodotCallable(callable).
The simpler cross-platform alternative needs no 4.4: Input.vibrate_handheld(duration_ms, amplitude) (requires the VIBRATE permission; iOS needs iOS 13+). See Plugins for the full JavaClassWrapper/AndroidRuntime API, Toast/Intent recipes, and inner-class syntax.
gdscript# Godot 4.4+ — requires the VIBRATE permission in the export preset. func vibrate_ms(duration_ms: int) -> void: if Engine.has_singleton("AndroidRuntime"): var runtime := Engine.get_singleton("AndroidRuntime") var context := runtime.getApplicationContext() var vibrator := context.getSystemService("vibrator") if vibrator.hasVibrator(): var Effect = JavaClassWrapper.wrap("android.os.VibrationEffect") var effect = Effect.createOneShot(duration_ms, Effect.DEFAULT_AMPLITUDE) vibrator.vibrate(effect)
The docs are GDScript-only here; the exact C# Variant-marshaling chain (.AsGodotObject() on each Java return) is untested against a device — verify on a real 4.4+ Android build.
csharp// Godot 4.4+ — requires the VIBRATE permission in the export preset. public void VibrateMs(int durationMs) { if (!Engine.HasSingleton("AndroidRuntime")) return; var runtime = Engine.GetSingleton("AndroidRuntime"); var context = runtime.Call("getApplicationContext").AsGodotObject(); var vibrator = context.Call("getSystemService", "vibrator").AsGodotObject(); if (vibrator.Call("hasVibrator").AsBool()) { var effect = JavaClassWrapper.Wrap("android.os.VibrationEffect"); var oneShot = effect.Call("createOneShot", durationMs, effect.Get("DEFAULT_AMPLITUDE")); vibrator.Call("vibrate", oneShot); } }
JavaClassWrapper.create_proxy(object: Object, interfaces: PackedStringArray) -> JavaObject implements the given Java interfaces using a Godot object — the object's method signatures must match the Java interfaces' method signatures, and Java calls route to the matching method. create_sam_callback(sam_interface: String, callable: Callable) -> JavaObject covers single-abstract-method (SAM) interfaces with a Callable matching the SAM method's parameters and return type. Both return null on every platform except Android.
gdscriptclass PrintProxy: func println(content: String) -> void: print(content) func _demo() -> void: var print_proxy := PrintProxy.new() var printer := JavaClassWrapper.create_proxy(print_proxy, ["android.util.Printer"]) printer.println("Hello Godot World!") var cb := func(content: String) -> void: print(content) var callback := JavaClassWrapper.create_sam_callback("android.util.Printer", cb) callback.println("Hello Godot World!")
The docs are GDScript-only here. CreateProxy matches methods by their registered name, so a C# implementation class would need method names matching the Java interface exactly — prefer CreateSamCallback from C#:
csharpvar cb = Callable.From((string content) => GD.Print(content)); var callback = JavaClassWrapper.CreateSamCallback("android.util.Printer", cb); callback.Call("println", "Hello Godot World!");
DisplayServer.get_display_safe_area() -> Rect2i (Android/iOS) returns the usable region inside notches/cutouts; get_display_cutouts() (Android) lists the cutout rects. Motion sensors live on Input (get_accelerometer/gravity/gyroscope/magnetometer, returning Vector3, Android/iOS only).
gdscriptfunc _ready(): var safe := DisplayServer.get_display_safe_area() # Rect2i $UI.position = safe.position $UI.size = safe.size
csharppublic override void _Ready() { Rect2I safe = DisplayServer.GetDisplaySafeArea(); var ui = GetNode<Control>("UI"); ui.Position = safe.Position; ui.Size = safe.Size; }
Native file dialogs (DisplayServer.file_dialog_show()) are supported on Android; since Godot 4.7 the native file picker works on all devices — the Android version check gating FEATURE_NATIVE_DIALOG_FILE support was removed (GH-115257).
> Godot 4.7+: For on-screen touch joysticks, use the built-in VirtualJoystick Control node instead of a hand-rolled TouchScreenButton rig — see input-handling (§ 6, "VirtualJoystick (Godot 4.7+)") for the full API.
DisplayServer.orientation_changed(orientation: int) (Android/iOS) fires when the device orientation changes: 1 portrait, 2 landscape, 0 undefined.
gdscriptfunc _ready() -> void: DisplayServer.orientation_changed.connect(_on_orientation_changed) func _on_orientation_changed(orientation: int) -> void: _relayout_hud(orientation == 2) # 1 portrait, 2 landscape, 0 undefined.
csharppublic override void _Ready() { DisplayServer.Singleton.Connect(DisplayServer.SignalName.OrientationChanged, Callable.From((long orientation) => RelayoutHud(orientation == 2))); }
Use the Mobile (or Compatibility) renderer; the iOS simulator is Compatibility-only. Enable rendering/textures/vram_compression/import_etc2_astc = true (ETC2/ASTC) for Android texture compression. Keep single-arch APKs small; AABs split per-device automatically. Defer deep draw-call/batching tuning to godot-optimization.
C# Android/iOS export is Godot 4.2+ but experimental. Android C# export requires .NET 9+ (with Godot 4.5); iOS export only from macOS, and the simulator templates are x64-only. C# cannot export to Web. Test the C# export pipeline early — it is the riskiest part of a C# mobile project.
> Deeper: Plugins (Android v2 / iOS) · In-app purchases & ads · Crash debugging
NOTIFICATION_APPLICATION_PAUSED (iOS ~5 s budget respected)get_display_safe_area(); notch/cutout handled| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-13 | pass→pass | 13,333 | 1,848 | -86% | 1 | 1 | 0% | 1,843 | 3,798 | +106% | 0 | 0 | — |
case-01 | fail→pass | 18,374 | 13,029 | -29% | 1 | 1 | 0% | 3,310 | 6,145 | +86% | 0 | 0 | — |
case-02 | fail→pass | 17,775 | 11,718 | -34% | 1 | 1 | 0% | 3,376 | 5,653 | +67% | 0 | 0 | — |
case-03 | fail→pass | 9,831 | 4,854 | -51% | 1 | 1 | 0% | 1,647 | 4,299 | +161% | 0 | 0 | — |
case-04 | pass→pass | 13,883 | 8,818 | -36% | 1 | 1 | 0% | 2,326 | 5,060 | +118% | 0 | 0 | — |
case-05 | pass→pass | 8,020 | 4,061 | -49% | 1 | 1 | 0% | 1,225 | 4,162 | +240% | 0 | 0 | — |
case-06 | fail→pass | 14,517 | 6,683 | -54% | 1 | 1 | 0% | 2,476 | 4,672 | +89% | 0 | 0 | — |
case-07 | fail→pass | 11,575 | 7,199 | -38% | 1 | 1 | 0% | 2,397 | 4,870 | +103% | 0 | 0 | — |
case-08 | fail→pass | 26,594 | 12,164 | -54% | 1 | 1 | 0% | 4,861 | 6,106 | +26% | 0 | 0 | — |
case-09 | fail→pass | 15,748 | 6,294 | -60% | 1 | 1 | 0% | 2,738 | 4,562 | +67% | 0 | 0 | — |
case-10 | fail→pass | 9,980 | 4,136 | -59% | 1 | 1 | 0% | 1,568 | 4,139 | +164% | 0 | 0 | — |
case-11 | fail→pass | 16,626 | 7,037 | -58% | 1 | 1 | 0% | 2,570 | 4,632 | +80% | 0 | 0 | — |
case-12 | pass→pass | 11,011 | 6,850 | -38% | 1 | 1 | 0% | 1,729 | 4,744 | +174% | 0 | 0 | — |
case-14 | fail→pass | 17,607 | 10,454 | -41% | 1 | 1 | 0% | 2,763 | 5,721 | +107% | 0 | 0 | — |
case-15 | pass→pass | 5,413 | 3,532 | -35% | 1 | 1 | 0% | 910 | 4,157 | +357% | 0 | 0 | — |
case-16 | fail→pass | 17,988 | 3,702 | -79% | 1 | 1 | 0% | 3,092 | 4,137 | +34% | 0 | 0 | — |
case-17 | fail→pass | 31,521 | 9,624 | -69% | 1 | 1 | 0% | 5,521 | 5,459 | -1% | 0 | 0 | — |
case-18 | fail→pass | 19,490 | 8,236 | -58% | 1 | 1 | 0% | 3,071 | 4,951 | +61% | 0 | 0 | — |
case-19 | pass→pass | 8,919 | 4,098 | -54% | 1 | 1 | 0% | 1,580 | 4,166 | +164% | 0 | 0 | — |
case-20 | pass→pass | 15,121 | 14,844 | -2% | 1 | 1 | 0% | 2,813 | 6,435 | +129% | 0 | 0 | — |
case-21 | pass→pass | 17,858 | 13,222 | -26% | 1 | 1 | 0% | 3,167 | 6,018 | +90% | 0 | 0 | — |
case-22 | pass→pass | 18,831 | 16,666 | -11% | 1 | 1 | 0% | 2,738 | 5,991 | +119% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +59 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.