| name | godot-adapt-mobile-to-desktop |
| description | Expert patterns for scaling mobile games to desktop including mouse/keyboard controls, increased resolution and graphical fidelity, expanded UI layouts, settings menus, window management, and platform-specific features. Use when creating desktop ports or cross-platform releases. Trigger keywords: mouse_controls, keyboard_shortcuts, resolution_scaling, graphics_settings, fullscreen_toggle, window_modes, Steam_integration, desktop_optimization. |
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.
Expert Mouse Capture Controller that completely overrides mobile touch/swipe logic by accumulating InputEventMouseMotion.relative against pitch/yaw variables while clamped.
Crucial lifecycle manager handling the expectation of PC gamers to toggle between Windowed, Fullscreen Exclusive, and modern Borderless Fullscreen via DisplayServer flags.
Complete runtime input remapper. Mobile relies on hardcoded touch zones, but PC requires the ability to swap WASD to custom keycodes via InputMap and saving to ConfigFile.
Hardware cursor state machine. Replaces the default OS arrow with custom Texture2D hardware cursors and handles hiding the cursor during combat while freeing it in menus.
Expert VSync and FPS unlocker. Mobile locks at 60FPS to save battery; PC gamers expect the ability to disable VSync and unlock Engine.max_fps for 144Hz+ monitors.
Query engine utilizing DisplayServer.screen_get_size to build an OptionButton of supported native 16:9, 21:9, and 4K resolutions without exceeding the user's physical monitor.
Recursive SceneTree crawler that scales down massive thumb-sized mobile buttons by a percentage shrink factor specifically on Desktop builds while retaining their anchor points.
Replaces the mobile Pinch-to-Zoom gesture with discrete physical mouse wheel ticks, smoothly interpolating the Camera2D zoom continuously via delta.
Hooks get_tree().set_auto_accept_quit(false) to intercept the OS-level 'X' window button, pausing the game and prompting the user to save instead of instantly terminating like mobile.
Advanced PC window placement script that queries the mouse position to identify the active screen on multi-monitor setups, ensuring the game launches exactly where the user is looking.
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
Expert Techniques & Optimizations
1. Standalone Desktop Launcher
For heavy desktop builds, use a lightweight Godot "Launcher" executable. This allows users to configure settings via ConfigFile before spawning the main game process using OS.create_process().
func launch_game() -> void:
# Save settings to user://settings.cfg
config.save("user://settings.cfg")
# Spawn main game with data pack argument
var path := OS.get_executable_path()
var args := PackedStringArray(["--main-pack", "game_data.pck"])
var pid := OS.create_process(path, args)
if pid > 0:
get_tree().quit()
2. Graphics Benchmark Mode
Automatically suggest optimal settings by running a 5-second performance test. Use ProjectSettings.set_setting() to toggle high-end features like Screen Space AA or SDFGI half-resolution based on average FPS.
func _evaluate_benchmark(frame_count: int, duration: float) -> void:
var avg_fps := frame_count / duration
if avg_fps < 30.0:
# Disable heavy features
ProjectSettings.set_setting("rendering/anti_aliasing/quality/screen_space_aa", 0)
ProjectSettings.set_setting("rendering/global_illumination/gi/use_half_resolution", true)
ProjectSettings.save_custom("user://override.cfg")
3. Steamworks Integration (GDExtension)
While Godot doesn't natively include Steamworks, the expert approach is to use a GDExtension (like GodotSteam) to bridge the Steam SDK for cloud saves and achievements. This keeps the core engine binary small while providing deep platform integration.
# Example using GodotSteam GDExtension
func _ready() -> void:
if Steam.steamInit().status == Steam.STEAM_OK:
print("Steam Cloud and Achievements Active")
Steam.requestStats()
Reference