skills/thedivergentai/gd-agentic-skills/godot-adapt-mobile-to-desktop

godot-adapt-mobile-to-desktop

SKILL.md

Adapt: Mobile to Desktop

Expert guidance for scaling mobile games to desktop platforms.

NEVER Do

  • NEVER keep touch-only controls — Add mouse/keyboard alternatives. Touch controls on desktop feel awkward and limit precision.
  • NEVER lock to mobile resolution — Desktop can handle 1920x1080+ and higher frame rates. Upscale UI, increase render distance.
  • NEVER hide graphics settings — Desktop players expect quality options (resolution, VSync, shadows, anti-aliasing).
  • NEVER use mobile-sized UI — Touch targets (44pt) are too large for mouse. Reduce button/text size by 30-50%.
  • NEVER forget window management — Players expect fullscreen, borderless, maximize, and multi-monitor support.

Available Scripts

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

desktop_input_adapter.gd

Bridges virtual joystick logic to keyboard WASD. Maps keys to input vectors and handles high-DPI UI scaling.

hover_bridge.gd

Re-enables desktop hover features in mobile-first codebases. Adds tooltips, mouse enter/exit signals, and hover visual feedback.


Control Scheme Expansion

Touch → Mouse Conversion

# Mobile: Virtual joystick for movement
var direction: Vector2 = virtual_joystick.get_direction()

# ⬇️ Desktop: WASD + mouse aim

extends CharacterBody2D

func _physics_process(delta: float) -> void:
    # Keyboard movement (WASD)
    var input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = input.normalized() * SPEED
    
    # Mouse aiming
    var mouse_pos := get_global_mouse_position()
    look_at(mouse_pos)
    
    move_and_slide()

# Configure Project Settings → Input Map:
# move_left: A, Left Arrow
# move_right: D, Right Arrow
# move_up: W, Up Arrow
# move_down: S, Down Arrow

Add Keyboard Shortcuts

# desktop_shortcuts.gd
extends Node

func _input(event: InputEvent) -> void:
    if event.is_action_pressed("toggle_fullscreen"):
        toggle_fullscreen()
    
    if event.is_action_pressed("quick_save"):
        save_game()
    
    if event.is_action_pressed("toggle_inventory"):
        $UI/Inventory.visible = not $UI/Inventory.visible

func toggle_fullscreen() -> void:
    if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
        DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
    else:
        DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)

# Add to Project Settings → Input Map:
# toggle_fullscreen: F11
# quick_save: F5
# toggle_inventory: I, Tab

Scroll Wheel Support

# Mobile: Pinch to zoom
# Desktop: Scroll wheel

func _input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_WHEEL_UP:
            camera.zoom *= 1.1
        elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
            camera.zoom *= 0.9

Graphics Enhancement

Resolution Scaling

# mobile_settings.gd (mobile)
func _ready() -> void:
    get_viewport().size = Vector2i(1280, 720)  # Mobile resolution

# ⬇️ desktop_settings.gd (desktop)

extends Node

@export var supported_resolutions: Array[Vector2i] = [
    Vector2i(1280, 720),
    Vector2i(1920, 1080),
    Vector2i(2560, 1440),
    Vector2i(3840, 2160)
]

func _ready() -> void:
    if OS.get_name() in ["Windows", "macOS", "Linux"]:
        # Start at native resolution
        var screen_size := DisplayServer.screen_get_size()
        get_window().size = screen_size
        
        # Enable higher  quality
        enable_desktop_graphics()

func enable_desktop_graphics() -> void:
    # Enable MSAA
    get_viewport().msaa_2d = Viewport.MSAA_2X
    get_viewport().msaa_3d = Viewport.MSAA_4X
    
    # Enable screen space AA
    get_viewport().screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
    
    # Higher shadow resolution
    RenderingServer.directional_shadow_atlas_set_size(4096, true)
    
    # Enable post-processing
    var env := get_viewport().world_3d.environment
    if env:
        env.glow_enabled = true
        env.ssao_enabled = true
        env.adjustment_enabled = true

Settings Menu

# graphics_settings.gd
extends Control

@onready var resolution_option: OptionButton = $VBoxContainer/Resolution
@onready var quality_option: OptionButton = $VBoxContainer/Quality
@onready var vsync_check: CheckBox = $VBoxContainer/VSync
@onready var fullscreen_check: CheckBox = $VBoxContainer/Fullscreen

func _ready() -> void:
    populate_settings()
    load_settings()

func populate_settings() -> void:
    # Resolution options
    resolution_option.add_item("1280x720")
    resolution_option.add_item("1920x1080")
    resolution_option.add_item("2560x1440")
    resolution_option.add_item("3840x2160")
    
    # Quality presets
    quality_option.add_item("Low")
    quality_option.add_item("Medium")
    quality_option.add_item("High")
    quality_option.add_item("Ultra")

func _on_resolution_selected(index: int) -> void:
    var resolutions := [
        Vector2i(1280, 720),
        Vector2i(1920, 1080),
        Vector2i(2560, 1440),
        Vector2i(3840, 2160)
    ]
    
    get_window().size = resolutions[index]
    save_settings()

func _on_quality_selected(index: int) -> void:
    match index:
        0:  # Low
            apply_low_quality()
        1:  # Medium
            apply_medium_quality()
        2:  # High
            apply_high_quality()
        3:  # Ultra
            apply_ultra_quality()
    
    save_settings()

func apply_ultra_quality() -> void:
    get_viewport().msaa_3d = Viewport.MSAA_8X
    get_viewport().screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
    RenderingServer.directional_shadow_atlas_set_size(8192, true)
    
    var env := get_viewport().world_3d.environment
    if env:
        env.glow_enabled = true
        env.ssao_enabled = true
        env.ssil_enabled = true
        env.sdfgi_enabled = true

func _on_vsync_toggled(enabled: bool) -> void:
    if enabled:
        DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
    else:
        DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
    
    save_settings()

func _on_fullscreen_toggled(enabled: bool) -> void:
    if enabled:
        DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
    else:
        DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
    
    save_settings()

func save_settings() -> void:
    var config := ConfigFile.new()
    config.set_value("graphics", "resolution_index", resolution_option.selected)
    config.set_value("graphics", "quality", quality_option.selected)
    config.set_value("graphics", "vsync", vsync_check.button_pressed)
    config.set_value("graphics", "fullscreen", fullscreen_check.button_pressed)
    config.save("user://settings.cfg")

func load_settings() -> void:
    var config := ConfigFile.new()
    if config.load("user://settings.cfg") == OK:
        resolution_option.selected = config.get_value("graphics", "resolution_index", 1)
        quality_option.selected = config.get_value("graphics", "quality", 2)
        vsync_check.button_pressed = config.get_value("graphics", "vsync", true)
        fullscreen_check.button_pressed = config.get_value("graphics", "fullscreen", false)
        
        # Apply settings
        _on_resolution_selected(resolution_option.selected)
        _on_quality_selected(quality_option.selected)
        _on_vsync_toggled(vsync_check.button_pressed)
        _on_fullscreen_toggled(fullscreen_check.button_pressed)

UI Layout Expansion

Mobile UI → Desktop UI

# Mobile: Compact HUD, large touch buttons
# Scene: MobileHUD.tscn
# - Virtual joystick (bottom-left)
# - Action buttons (bottom-right, 80x80px)

# ⬇️ Desktop: Spread UI, smaller elements

# Scene: DesktopHUD.tscn
# - Health/Mana bars (top-left, 40px tall)
# - Minimap (top-right, 200x200px)
# - Hotbar (bottom-center, 50x50px slots)
# - Chat (bottom-left, resizable)

extends Control

func _ready() -> void:
    if OS.has_feature("mobile"):
        _setup_mobile_ui()
    else:
        _setup_desktop_ui()

func _setup_mobile_ui() -> void:
    # Large buttons, bottom corners
    $VirtualJoystick.visible = true
    $ActionButtons.scale = Vector2(1.5, 1.5)
    $Minimap.visible = false  # Too cluttered

func _setup_desktop_ui() -> void:
    # Compact, corners and edges
    $VirtualJoystick.visible = false
    $ActionButtons.scale = Vector2(0.8, 0.8)
    $Minimap.visible = true
    $ChatBox.visible = true

Window Management

Multi-Monitor Support

# window_manager.gd
extends Node

func _ready() -> void:
    # Detect monitors
    var screen_count := DisplayServer.get_screen_count()
    print("Detected %d monitors" % screen_count)
    
    # Allow window dragging between monitors
    DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)

func move_to_monitor(monitor_index: int) -> void:
    var screen_pos := DisplayServer.screen_get_position(monitor_index)
    var screen_size := DisplayServer.screen_get_size(monitor_index)
    
    # Center window on target monitor
    var window_size := get_window().size
    var centered_pos := screen_pos + (screen_size - window_size) / 2
    
    DisplayServer.window_set_position(centered_pos)

Borderless Fullscreen

func set_borderless_fullscreen(enabled: bool) -> void:
    if enabled:
        # Get screen size
        var screen_size := DisplayServer.screen_get_size()
        
        # Set window to screen size
        get_window().size = screen_size
        get_window().position = Vector2i.ZERO
        
        # Remove border
        DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
    else:
        DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
        DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)

Platform-Specific Features

Steam Integration (Example)

# Requires GodotSteam plugin
extends Node

var steam_initialized := false

func _ready() -> void:
    if OS.get_name() in ["Windows", "Linux", "macOS"]:
        initialize_steam()

func initialize_steam() -> void:
    var init_result := Steam.steamInit()
    if init_result.status == Steam.STEAM_OK:
        steam_initialized = true
        print("Steam initialized")
        
        # Enable achievements
        Steam.requestStats()

func unlock_achievement(achievement_id: String) -> void:
    if steam_initialized:
        Steam.setAchievement(achievement_id)
        Steam.storeStats()

Discord Rich Presence

# Requires Discord SDK integration
extends Node

func update_presence(state: String, details: String) -> void:
    if OS.get_name() == "Windows":
        # Update Discord presence
        # (Requires plugin)
        pass

Performance Enhancements

Unlock Frame Rate

# Mobile: Locked to 60 FPS
Engine.max_fps = 60

# Desktop: Unlock or  match monitor refresh rate
func _ready() -> void:
    if not OS.has_feature("mobile"):
        Engine.max_fps = 0  # Unlimited (use VSync to cap)
        
        # Or match monitor:
        var refresh_rate := DisplayServer.screen_get_refresh_rate()
        Engine.max_fps = int(refresh_rate)

Increased Draw Distance

# Mobile: Low draw distance
var camera: Camera3D
camera.far = 100.0

# Desktop: Higher
camera.far = 500.0

# Also increase shadow distance
var sun: DirectionalLight3D
sun.directional_shadow_max_distance = 200.0  # Up from 50

Testing Checklist

  • Mouse controls feel precise (no acceleration issues)
  • All mobile touch controls have keyboard/mouse equivalents
  • Graphics settings menu works correctly
  • Fullscreen, windowed, borderless modes all function
  • Multi-monitor setup works (dragging window, centering)
  • Resolution changes don't crash or distort UI
  • VSync toggle works
  • Runs at 144+ FPS on high-end hardware
  • Settings persist across sessions
  • Game scales well to ultrawide monitors (21:9, 32:9)

Reference

Weekly Installs
43
GitHub Stars
35
First Seen
Feb 10, 2026
Installed on
gemini-cli43
codex42
opencode41
kimi-cli40
amp40
github-copilot40