Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot Adapt Desktop To Mobile

  • 184 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

Use godot-adapt-desktop-to-mobile for development tasks

About

godot-adapt-desktop-to-mobile: A skill for development. This provides functionality for development workflows.

  • godot-adapt-desktop-to-mobile

Godot Adapt Desktop To Mobile by the numbers

  • 184 all-time installs (skills.sh)
  • +10 installs in the week ending Jul 27, 2026 (Skillselion tracking)
  • Ranked #2,160 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-adapt-desktop-to-mobile

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs184
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

What it does

Use godot-adapt-desktop-to-mobile for development tasks

Files

SKILL.mdMarkdownGitHub ↗

Adapt: Desktop to Mobile

Expert guidance for porting desktop games to mobile platforms.

NEVER Do

  • NEVER use mouse position directly — Touch has no "hover" state. Replace mouse_motion with screen_drag and check InputEventScreenTouch.pressed.
  • NEVER keep small UI elements — Apple HIG requires 44pt minimum touch targets. Android Material: 48dp. Scale up buttons 2-3x.
  • NEVER forget finger occlusion — User's finger blocks 50-100px radius. Position critical info ABOVE touch controls, not below.
  • NEVER run at full performance when backgrounded — Mobile OSs kill apps that drain battery in background. Pause physics, reduce FPS to 1-5 when app loses focus.
  • NEVER use desktop-only features — Mouse hover, right-click, keyboard shortcuts, scroll wheel don't exist on mobile. Provide touch alternatives.

---

Available Scripts

MANDATORY: Read the appropriate script before implementing the corresponding pattern.

dynamic_joystick_spawner.gd

Expert Dynamic Virtual Joystick that appears exactly where the user touches the left half of the screen instead of relying on fixed UI positions.

resolution_scaler.gd

Adaptive Viewport scaler that dynamically drops the scaling_3d_scale to maintain 60FPS on weak GPUs while keeping the 2D UI perfectly sharp.

gesture_combo_system.gd

Advanced touch gesture recognizer tracking duration, distance, and multi-touch ratios to output precise swipe and pinch-to-zoom signals.

battery_saver_mode.gd

Crucial lifecycle manager that hooks NOTIFICATION_APPLICATION_PAUSED to instantly lock Engine.max_fps = 1 and pause physics to prevent the OS from killing the app due to background battery drain.

ui_safe_area_margins.gd

Dynamic MarginContainer script querying DisplayServer.get_display_safe_area() to automatically pad UI elements around iPhone notches and Android hole-punch cameras.

touch_camera_pan_zoom.gd

Smooth Camera2D controller combining 1-finger relative panning and 2-finger distance-ratio pinch zooming simultaneously.

haptic_feedback_manager.gd

Centralized singleton triggering Input.vibrate_handheld for Android, and demonstrating the hook pattern for iOS native haptic plugins.

mobile_shader_fallback.gd

SceneTree crawler that strips expensive sub-surface scattering, clearcoats, and dynamic shading from StandardMaterial3D on weak mobile renderers.

on_screen_keyboard_handler.gd

Listens to DisplayServer.virtual_keyboard_update to tween the entire UI upward, preventing the OS keyboard from occluding LineEdits.

offline_save_sync.gd

Memory-based save dictionary that bypasses NOTIFICATION_WM_CLOSE_REQUEST (which fails on mobile kill) and guarantees encrypted disk writes during the App Pause lifecycle.

---

Touch Control Schemes

Decision Matrix

GenreRecommended ControlExample
PlatformerVirtual joystick (left) + jump button (right)Super Mario Run
Top-down shooterDual-stick (move left, aim right)Brawl Stars
Turn-basedDirect tap on units/tilesInto the Breach
PuzzleTap, swipe, pinch gesturesCandy Crush
Card gameDrag-and-dropHearthstone
RacingTilt steering or tap left/rightAsphalt 9

Virtual Joystick

# virtual_joystick.gd
extends Control

signal direction_changed(direction: Vector2)

@export var dead_zone: float = 0.2
@export var max_distance: float = 100.0

var stick_center: Vector2
var is_pressed: bool = false
var touch_index: int = -1

@onready var base: Sprite2D = $Base
@onready var knob: Sprite2D = $Knob

func _ready() -> void:
    stick_center = base.position

func _input(event: InputEvent) -> void:
    if event is InputEventScreenTouch:
        if event.pressed and is_point_inside(event.position):
            is_pressed = true
            touch_index = event.index
        elif not event.pressed and event.index == touch_index:
            is_pressed = false
            reset_knob()
    
    elif event is InputEventScreenDrag and event.index == touch_index:
        update_knob(event.position)

func is_point_inside(point: Vector2) -> bool:
    return base.get_rect().has_point(base.to_local(point))

func update_knob(touch_pos: Vector2) -> void:
    var local_pos := to_local(touch_pos)
    var offset := local_pos - stick_center
    
    # Clamp to max distance
    if offset.length() > max_distance:
        offset = offset.normalized() * max_distance
    
    knob.position = stick_center + offset
    
    # Calculate direction (-1 to 1)
    var direction := offset / max_distance
    if direction.length() < dead_zone:
        direction = Vector2.ZERO
    
    direction_changed.emit(direction)

func reset_knob() -> void:
    knob.position = stick_center
    direction_changed.emit(Vector2.ZERO)

Gesture Detection

# gesture_detector.gd
extends Node

signal swipe_detected(direction: Vector2)  # Normalized
signal pinch_detected(scale: float)  # > 1.0 = zoom in
signal tap_detected(position: Vector2)

const SWIPE_THRESHOLD := 100.0  # Pixels
const TAP_MAX_DISTANCE := 20.0
const TAP_MAX_DURATION := 0.3  # Seconds

var touch_start: Dictionary = {}  # index → {position: Vector2, time: float}
var pinch_start_distance: float = 0.0

func _input(event: InputEvent) -> void:
    if event is InputEventScreenTouch:
        if event.pressed:
            touch_start[event.index] = {
                "position": event.position,
                "time": Time.get_ticks_msec() * 0.001
            }
        else:
            _handle_release(event)
    
    elif event is InputEventScreenDrag:
        _handle_drag(event)

func _handle_release(event: InputEventScreenTouch) -> void:
    if event.index not in touch_start:
        return
    
    var start_data = touch_start[event.index]
    var distance := event.position.distance_to(start_data.position)
    var duration := (Time.get_ticks_msec() * 0.001) - start_data.time
    
    # Tap detection
    if distance < TAP_MAX_DISTANCE and duration < TAP_MAX_DURATION:
        tap_detected.emit(event.position)
    
    # Swipe detection
    elif distance > SWIPE_THRESHOLD:
        var direction := (event.position - start_data.position).normalized()
        swipe_detected.emit(direction)
    
    touch_start.erase(event.index)

func _handle_drag(event: InputEventScreenDrag) -> void:
    # Pinch detection (requires 2 touches)
    if touch_start.size() == 2:
        var positions := []
        for idx in touch_start.keys():
            if idx == event.index:
                positions.append(event.position)
            else:
                positions.append(touch_start[idx].position)
        
        var current_distance := positions[0].distance_to(positions[1])
        
        if pinch_start_distance == 0.0:
            pinch_start_distance = current_distance
        else:
            var scale := current_distance / pinch_start_distance
            pinch_detected.emit(scale)
            pinch_start_distance = current_distance

---

UI Scaling

Responsive Layout

# Adjust UI for different screen sizes
extends Control

func _ready() -> void:
    get_viewport().size_changed.connect(_on_viewport_resized)
    _on_viewport_resized()

func _on_viewport_resized() -> void:
    var viewport_size := get_viewport_rect().size
    var aspect_ratio := viewport_size.x / viewport_size.y
    
    # Adjust for different aspect ratios
    if aspect_ratio > 2.0:  # Ultra-wide (tablets in landscape)
        scale_ui_for_tablet()
    elif aspect_ratio < 0.6:  # Tall (phones in portrait)
        scale_ui_for_phone()
    
    # Adjust touch button sizes
    for button in get_tree().get_nodes_in_group("touch_buttons"):
        var min_size := 88  # 44pt * 2 for Retina
        button.custom_minimum_size = Vector2(min_size, min_size)

func scale_ui_for_tablet() -> void:
    # Spread UI to edges, use horizontal space
    $LeftControls.position.x = 100
    $RightControls.position.x = get_viewport_rect().size.x - 100

func scale_ui_for_phone() -> void:
    # Keep  UI at bottom, vertically compact
    $LeftControls.position.y = get_viewport_rect().size.y - 200
    $RightControls.position.y = get_viewport_rect().size.y - 200

---

Performance Optimization

Mobile-Specific Settings

# project.godot or autoload
extends Node

func _ready() -> void:
    if OS.get_name() in ["Android", "iOS"]:
        apply_mobile_optimizations()

func apply_mobile_optimizations() -> void:
    # Reduce rendering quality
    get_viewport().msaa_2d = Viewport.MSAA_DISABLED
    get_viewport().msaa_3d = Viewport.MSAA_DISABLED
    get_viewport().screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
    
    # Lower shadow quality
    RenderingServer.directional_shadow_atlas_set_size(2048, false)  # Down from 4096
    
    # Reduce particle counts
    for particle in get_tree().get_nodes_in_group("godot-particles"):
        if particle is GPUParticles2D:
            particle.amount = max(10, particle.amount / 2)
    
    # Lower physics tick rate
    Engine.physics_ticks_per_second = 30  # Down from 60
    
    # Disable expensive effects
    var env := get_viewport().world_3d.environment
    if env:
        env.glow_enabled = false
        env.ssao_enabled = false
        env.ssr_enabled = false

Adaptive Performance

# Dynamically adjust quality based on FPS
extends Node

@export var target_fps: int = 60
@export var check_interval: float = 2.0

var timer: float = 0.0
var quality_level: int = 2  # 0=low, 1=med, 2=high

func _process(delta: float) -> void:
    timer += delta
    if timer >= check_interval:
        var current_fps := Engine.get_frames_per_second()
        
        if current_fps < target_fps - 10 and quality_level > 0:
            quality_level -= 1
            apply_quality(quality_level)
        elif current_fps > target_fps + 5 and quality_level < 2:
            quality_level += 1
            apply_quality(quality_level)
        
        timer = 0.0

func apply_quality(level: int) -> void:
    match level:
        0:  # Low
            get_viewport().scaling_3d_scale = 0.5
        1:  # Medium
            get_viewport().scaling_3d_scale = 0.75
        2:  # High
            get_viewport().scaling_3d_scale = 1.0

---

Battery Life Management

Background Behavior

#  mobile_lifecycle.gd
extends Node

func _ready() -> void:
    get_tree().on_request_permissions_result.connect(_on_permissions_result)

func _notification(what: int) -> void:
    match what:
        NOTIFICATION_APPLICATION_PAUSED:
            _on_app_backgrounded()
        NOTIFICATION_APPLICATION_RESUMED:
            _on_app_foregrounded()

func _on_app_backgrounded() -> void:
    # Reduce FPS drastically
    Engine.max_fps = 5
    
    # Pause physics
    get_tree().paused = true
    
    # Stop audio
    AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), true)

func _on_app_foregrounded() -> void:
    # Restore FPS
    Engine.max_fps = 60
    
    # Resume
    get_tree().paused = false
    AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), false)

---

Platform-Specific Features

Safe Area Insets (iPhone Notch)

# Handle notch/status bar
func _ready() -> void:
    if OS.get_name() == "iOS":
        var safe_area := DisplayServer.get_display_safe_area()
        var viewport_size := get_viewport_rect().size
        
        # Adjust UI margins
        $TopBar.position.y = safe_area.position.y
        $BottomControls.position.y = viewport_size.y - safe_area.end.y - 100

Vibration Feedback

func trigger_haptic(intensity: float) -> void:
    if OS.has_feature("mobile"):
        # Android
        if OS.get_name() == "Android":
            var duration_ms := int(intensity * 100)
            OS.vibrate_handheld(duration_ms)
        
        # iOS (requires plugin)
        # Use third-party plugin for iOS haptics

---

Input Remapping

Mouse → Touch Conversion

# Desktop mouse input
func _input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed:
        _on_click(event.position)

# ⬇️ Convert to touch:

func _input(event: InputEvent) -> void:
    # Support both mouse (desktop testing) and touch
    if event is InputEventMouseButton and event.pressed:
        _on_click(event.position)
    elif event is InputEventScreenTouch and event.pressed:
        _on_click(event.position)

func _on_click(position: Vector2) -> void:
    # Handle click/tap
    pass

---

Edge Cases

Keyboard Popup Blocking UI

# Problem: Virtual keyboard covers text input
# Solution: Detect keyboard, scroll UI up

func _on_text_edit_focus_entered() -> void:
    if OS.has_feature("mobile"):
        # Keyboard height varies; estimate 300px
        var keyboard_offset := 300
        $UI.position.y -= keyboard_offset

func _on_text_edit_focus_exited() -> void:
    $UI.position.y = 0

Accidental Touch Inputs

# Problem: Palm resting on screen triggers inputs
# Solution: Ignore touches near screen edges

func is_valid_touch(position: Vector2) -> bool:
    var viewport_size := get_viewport_rect().size
    var edge_margin := 50.0
    
    return (position.x > edge_margin and
            position.x < viewport_size.x - edge_margin and
            position.y > edge_margin and
            position.y < viewport_size.y - edge_margin)

---

Testing Checklist

  • [ ] Touch controls work with fat fingers (test on real device)
  • [ ] UI doesn't block gameplay-critical elements
  • [ ] Game pauses when app goes to background
  • [ ] Performance is 60 FPS on target device (iPhone 12, Galaxy S21)
  • [ ] Battery drain is < 10% per hour
  • [ ] Safe area respected (notch, status bar)
  • [ ] Works in both portrait and landscape
  • [ ] Text is readable on smallest target device (iPhone SE)

Expert Techniques & Optimizations

1. Unified IAP Manager (iOS & Android)

Mobile In-App Purchases (IAP) rely on platform-specific singletons (GodotGooglePlayBilling for Android and InAppStore for iOS). Use feature tags to abstract these behind a unified interface.

class_name IAPManager extends Node

var android_billing: Object
var ios_store: Object

func _ready() -> void:
    if OS.has_feature("android") and Engine.has_singleton("GodotGooglePlayBilling"):
        android_billing = Engine.get_singleton("GodotGooglePlayBilling")
        android_billing.start_connection()
    elif OS.has_feature("ios") and Engine.has_singleton("InAppStore"):
        ios_store = Engine.get_singleton("InAppStore")
        ios_store.set_auto_finish_transaction(true)

func purchase_product(id: String) -> void:
    if android_billing:
        android_billing.purchase(id)
    elif ios_store:
        ios_store.purchase({ "product_id": id })

2. App Store Asset Pipeline (Screenshot Automation)

Generating store screenshots across multiple resolutions is tedious. Automate this by capturing the viewport texture after the frame is drawn.

func capture_screenshot(filename: String) -> void:
    # Wait for the frame to finish rendering
    await RenderingServer.frame_post_draw
    
    var img: Image = get_viewport().get_texture().get_image()
    var path := "user://screenshots/%s.png" % filename
    img.save_png(path)
    print("Screenshot saved to: ", path)

3. Mobile Debug Overlay (Memory Monitoring)

Mobile devices have strict memory limits. Use OS.get_memory_info() to monitor physical and available memory in real-time.

func _process(_delta: float) -> void:
    var mem: Dictionary = OS.get_memory_info()
    var physical_mb := float(mem.get("physical", 0)) / 1048576.0
    var available_mb := float(mem.get("available", 0)) / 1048576.0
    
    $DebugLabel.text = "Mem: %.2f / %.2f MB" % [available_mb, physical_mb]

Reference

  • Master Skill: godot-master

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.