Claude
Skills
Sign in
Back

godot-adapt-desktop-to-mobile

Included with Lifetime
$97 forever

Expert patterns for porting desktop games to mobile including touch control schemes (virtual joystick, gesture detection), UI scaling for small screens, performance optimization for mobile GPUs, battery life management, and platform-specific features. Use when creating mobile ports or cross-platform mobile builds. Trigger keywords: TouchScreenButton, virtual_joystick, gesture_detector, InputEventScreenTouch, InputEventScreenDrag, mobile_optimization, battery_saving, adaptive_performance, MOBILE_ENABLED.

Designscripts

What this skill does


# 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](scripts/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](scripts/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](scripts/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](scripts/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](scripts/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](scripts/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](scripts/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](scripts/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](scripts/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](scripts/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

| Genre | Recommended Control | Example |
|-------|-------------------|---------|
| Platformer | Virtual joystick (left) + jump button (right) | Super Mario Run |
| Top-down shooter | Dual-stick (move left, aim right) | Brawl Stars |
| Turn-based | Direct tap on units/tiles | Into the Breach |
| Puzzle | Tap, swipe, pinch gestures | Candy Crush |
| Card game | Drag-and-drop | Hearthstone |
| Racing | Tilt steering or tap left/right | Asphalt 9 |

### Virtual Joystick

```gdscript
# 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

```gdscript
# 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

```gdscript
# 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()
    
    # Ad

Related in Design