Skill v1.0.1
currentAutomated scan100/10013 files
version: "1.0.1" name: godot-adapt-desktop-to-mobile description: "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."
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
| 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
# virtual_joystick.gdextends Controlsignal direction_changed(direction: Vector2)@export var dead_zone: float = 0.2@export var max_distance: float = 100.0var stick_center: Vector2var is_pressed: bool = falsevar touch_index: int = -1@onready var base: Sprite2D = $Base@onready var knob: Sprite2D = $Knobfunc _ready() -> void:stick_center = base.positionfunc _input(event: InputEvent) -> void:if event is InputEventScreenTouch:if event.pressed and is_point_inside(event.position):is_pressed = truetouch_index = event.indexelif not event.pressed and event.index == touch_index:is_pressed = falsereset_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 distanceif offset.length() > max_distance:offset = offset.normalized() * max_distanceknob.position = stick_center + offset# Calculate direction (-1 to 1)var direction := offset / max_distanceif direction.length() < dead_zone:direction = Vector2.ZEROdirection_changed.emit(direction)func reset_knob() -> void:knob.position = stick_centerdirection_changed.emit(Vector2.ZERO)
Gesture Detection
# gesture_detector.gdextends Nodesignal swipe_detected(direction: Vector2) # Normalizedsignal pinch_detected(scale: float) # > 1.0 = zoom insignal tap_detected(position: Vector2)const SWIPE_THRESHOLD := 100.0 # Pixelsconst TAP_MAX_DISTANCE := 20.0const TAP_MAX_DURATION := 0.3 # Secondsvar touch_start: Dictionary = {} # index → {position: Vector2, time: float}var pinch_start_distance: float = 0.0func _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:returnvar 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 detectionif distance < TAP_MAX_DISTANCE and duration < TAP_MAX_DURATION:tap_detected.emit(event.position)# Swipe detectionelif 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_distanceelse:var scale := current_distance / pinch_start_distancepinch_detected.emit(scale)pinch_start_distance = current_distance
UI Scaling
Responsive Layout
# Adjust UI for different screen sizesextends Controlfunc _ready() -> void:get_viewport().size_changed.connect(_on_viewport_resized)_on_viewport_resized()func _on_viewport_resized() -> void:var viewport_size := get_viewport_rect().sizevar aspect_ratio := viewport_size.x / viewport_size.y# Adjust for different aspect ratiosif 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 sizesfor button in get_tree().get_nodes_in_group("touch_buttons"):var min_size := 88 # 44pt * 2 for Retinabutton.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 - 100func 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 autoloadextends Nodefunc _ready() -> void:if OS.get_name() in ["Android", "iOS"]:apply_mobile_optimizations()func apply_mobile_optimizations() -> void:# Reduce rendering qualityget_viewport().msaa_2d = Viewport.MSAA_DISABLEDget_viewport().msaa_3d = Viewport.MSAA_DISABLEDget_viewport().screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED# Lower shadow qualityRenderingServer.directional_shadow_atlas_set_size(2048, false) # Down from 4096# Reduce particle countsfor particle in get_tree().get_nodes_in_group("godot-particles"):if particle is GPUParticles2D:particle.amount = max(10, particle.amount / 2)# Lower physics tick rateEngine.physics_ticks_per_second = 30 # Down from 60# Disable expensive effectsvar env := get_viewport().world_3d.environmentif env:env.glow_enabled = falseenv.ssao_enabled = falseenv.ssr_enabled = false
Adaptive Performance
# Dynamically adjust quality based on FPSextends Node@export var target_fps: int = 60@export var check_interval: float = 2.0var timer: float = 0.0var quality_level: int = 2 # 0=low, 1=med, 2=highfunc _process(delta: float) -> void:timer += deltaif timer >= check_interval:var current_fps := Engine.get_frames_per_second()if current_fps < target_fps - 10 and quality_level > 0:quality_level -= 1apply_quality(quality_level)elif current_fps > target_fps + 5 and quality_level < 2:quality_level += 1apply_quality(quality_level)timer = 0.0func apply_quality(level: int) -> void:match level:0: # Lowget_viewport().scaling_3d_scale = 0.51: # Mediumget_viewport().scaling_3d_scale = 0.752: # Highget_viewport().scaling_3d_scale = 1.0
Battery Life Management
Background Behavior
# mobile_lifecycle.gdextends Nodefunc _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 drasticallyEngine.max_fps = 5# Pause physicsget_tree().paused = true# Stop audioAudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), true)func _on_app_foregrounded() -> void:# Restore FPSEngine.max_fps = 60# Resumeget_tree().paused = falseAudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), false)
Platform-Specific Features
Safe Area Insets (iPhone Notch)
# Handle notch/status barfunc _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"):# Androidif 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 inputfunc _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 touchif 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/tappass
Edge Cases
Keyboard Popup Blocking UI
# Problem: Virtual keyboard covers text input# Solution: Detect keyboard, scroll UI upfunc _on_text_edit_focus_entered() -> void:if OS.has_feature("mobile"):# Keyboard height varies; estimate 300pxvar keyboard_offset := 300$UI.position.y -= keyboard_offsetfunc _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 edgesfunc is_valid_touch(position: Vector2) -> bool:var viewport_size := get_viewport_rect().sizevar edge_margin := 50.0return (position.x > edge_margin andposition.x < viewport_size.x - edge_margin andposition.y > edge_margin andposition.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 Nodevar android_billing: Objectvar ios_store: Objectfunc _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 renderingawait RenderingServer.frame_post_drawvar img: Image = get_viewport().get_texture().get_image()var path := "user://screenshots/%s.png" % filenameimg.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.0var available_mb := float(mem.get("available", 0)) / 1048576.0$DebugLabel.text = "Mem: %.2f / %.2f MB" % [available_mb, physical_mb]
Reference
- Master Skill: godot-master